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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import { SystemDataCollector } from './SystemDataCollector';
import { PerformanceDataCollector } from './PerformanceDataCollector';
import { ComponentDataCollector } from './ComponentDataCollector';
import { SceneDataCollector } from './SceneDataCollector';
import { AdvancedProfilerCollector } from './AdvancedProfilerCollector';
import { WebSocketManager } from './WebSocketManager';
import { Component } from '../../ECS/Component';
import { ComponentPoolManager } from '../../ECS/Core/ComponentPool';
@@ -15,6 +16,7 @@ import { SceneManager } from '../../ECS/SceneManager';
import { PerformanceMonitor } from '../PerformanceMonitor';
import { Injectable, InjectProperty, Updatable } from '../../Core/DI/Decorators';
import { DebugConfigService } from './DebugConfigService';
import { ProfilerSDK } from '../Profiler/ProfilerSDK';
/**
* 调试管理器
@@ -31,6 +33,7 @@ export class DebugManager implements IService, IUpdatable {
private performanceCollector!: PerformanceDataCollector;
private componentCollector!: ComponentDataCollector;
private sceneCollector!: SceneDataCollector;
private advancedProfilerCollector!: AdvancedProfilerCollector;
@InjectProperty(SceneManager)
private sceneManager!: SceneManager;
@@ -62,6 +65,10 @@ export class DebugManager implements IService, IUpdatable {
this.performanceCollector = new PerformanceDataCollector();
this.componentCollector = new ComponentDataCollector();
this.sceneCollector = new SceneDataCollector();
this.advancedProfilerCollector = new AdvancedProfilerCollector();
// 启用高级性能分析器
ProfilerSDK.setEnabled(true);
// 初始化WebSocket管理器
this.webSocketManager = new WebSocketManager(
@@ -290,6 +297,14 @@ export class DebugManager implements IService, IUpdatable {
this.handleGetEntityDetailsRequest(message);
break;
case 'get_advanced_profiler_data':
this.handleGetAdvancedProfilerDataRequest(message);
break;
case 'set_profiler_selected_function':
this.handleSetProfilerSelectedFunction(message);
break;
case 'ping':
this.webSocketManager.send({
type: 'pong',
@@ -437,6 +452,54 @@ export class DebugManager implements IService, IUpdatable {
}
/**
* 处理获取高级性能分析数据请求
*/
private handleGetAdvancedProfilerDataRequest(message: any): void {
try {
const { requestId } = message;
// 收集高级性能数据
const advancedData = ProfilerSDK.isEnabled()
? this.advancedProfilerCollector.collectAdvancedData(this.performanceMonitor)
: this.advancedProfilerCollector.collectFromLegacyMonitor(this.performanceMonitor);
this.webSocketManager.send({
type: 'get_advanced_profiler_data_response',
requestId,
data: advancedData
});
} catch (error) {
this.webSocketManager.send({
type: 'get_advanced_profiler_data_response',
requestId: message.requestId,
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* 处理设置性能分析器选中函数请求
*/
private handleSetProfilerSelectedFunction(message: any): void {
try {
const { functionName, requestId } = message;
this.advancedProfilerCollector.setSelectedFunction(functionName || null);
this.webSocketManager.send({
type: 'set_profiler_selected_function_response',
requestId,
success: true
});
} catch (error) {
this.webSocketManager.send({
type: 'set_profiler_selected_function_response',
requestId: message.requestId,
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* 处理内存快照请求
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,342 @@
import { AdvancedProfilerCollector } from '../../../src/Utils/Debug/AdvancedProfilerCollector';
import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK';
import { ProfileCategory } from '../../../src/Utils/Profiler/ProfilerTypes';
describe('AdvancedProfilerCollector', () => {
let collector: AdvancedProfilerCollector;
beforeEach(() => {
collector = new AdvancedProfilerCollector();
ProfilerSDK.reset();
ProfilerSDK.setEnabled(true);
});
afterEach(() => {
ProfilerSDK.reset();
});
describe('collectAdvancedData', () => {
test('should collect basic frame data', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.measure('TestSystem', () => {
// Simulate work
let sum = 0;
for (let i = 0; i < 1000; i++) sum += i;
}, ProfileCategory.ECS);
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
expect(data).toBeDefined();
expect(data.currentFrame).toBeDefined();
expect(data.currentFrame.frameNumber).toBeGreaterThanOrEqual(0);
expect(data.currentFrame.fps).toBeGreaterThanOrEqual(0);
});
test('should collect category stats', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.measure('ECSSystem', () => {}, ProfileCategory.ECS);
ProfilerSDK.measure('RenderSystem', () => {}, ProfileCategory.Rendering);
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
expect(data.categoryStats).toBeDefined();
expect(data.categoryStats.length).toBeGreaterThan(0);
});
test('should collect hotspots sorted by time', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.measure('FastFunction', () => {}, ProfileCategory.Script);
ProfilerSDK.measure('SlowFunction', () => {
const start = performance.now();
while (performance.now() - start < 2) {
// busy wait
}
}, ProfileCategory.Script);
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
expect(data.hotspots).toBeDefined();
expect(data.hotspots.length).toBeGreaterThan(0);
});
test('should include frame time history', () => {
for (let i = 0; i < 5; i++) {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
}
const data = collector.collectAdvancedData();
expect(data.frameTimeHistory).toBeDefined();
expect(data.frameTimeHistory.length).toBeGreaterThan(0);
});
test('should include memory information', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
expect(data.currentFrame.memory).toBeDefined();
expect(data.currentFrame.memory.timestamp).toBeGreaterThan(0);
});
test('should include summary statistics', () => {
for (let i = 0; i < 10; i++) {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
}
const data = collector.collectAdvancedData();
expect(data.summary).toBeDefined();
expect(data.summary.totalFrames).toBeGreaterThan(0);
expect(typeof data.summary.averageFrameTime).toBe('number');
expect(typeof data.summary.minFrameTime).toBe('number');
expect(typeof data.summary.maxFrameTime).toBe('number');
});
test('should include long tasks list', () => {
const data = collector.collectAdvancedData();
expect(data.longTasks).toBeDefined();
expect(Array.isArray(data.longTasks)).toBe(true);
});
test('should include memory trend', () => {
const data = collector.collectAdvancedData();
expect(data.memoryTrend).toBeDefined();
expect(Array.isArray(data.memoryTrend)).toBe(true);
});
});
describe('setSelectedFunction', () => {
test('should set selected function for call graph', () => {
collector.setSelectedFunction('TestFunction');
ProfilerSDK.beginFrame();
const parentHandle = ProfilerSDK.beginSample('ParentFunction', ProfileCategory.Script);
const childHandle = ProfilerSDK.beginSample('TestFunction', ProfileCategory.Script);
ProfilerSDK.endSample(childHandle);
ProfilerSDK.endSample(parentHandle);
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
expect(data.callGraph).toBeDefined();
expect(data.callGraph.currentFunction).toBe('TestFunction');
});
test('should clear selected function with null', () => {
collector.setSelectedFunction('TestFunction');
collector.setSelectedFunction(null);
const data = collector.collectAdvancedData();
expect(data.callGraph.currentFunction).toBeNull();
});
test('should return empty callers/callees when no function selected', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.measure('Test', () => {}, ProfileCategory.Script);
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
expect(data.callGraph.currentFunction).toBeNull();
expect(data.callGraph.callers).toEqual([]);
expect(data.callGraph.callees).toEqual([]);
});
});
describe('collectFromLegacyMonitor', () => {
test('should handle null performance monitor', () => {
const data = collector.collectFromLegacyMonitor(null);
expect(data).toBeDefined();
expect(data.currentFrame.frameNumber).toBe(0);
expect(data.categoryStats).toEqual([]);
expect(data.hotspots).toEqual([]);
});
test('should build data from legacy monitor', () => {
const mockMonitor = {
getAllSystemStats: () => new Map([
['TestSystem', {
averageTime: 5,
minTime: 2,
maxTime: 10,
executionCount: 100
}]
]),
getAllSystemData: () => new Map([
['TestSystem', {
executionTime: 5,
entityCount: 50
}]
])
};
const data = collector.collectFromLegacyMonitor(mockMonitor);
expect(data.categoryStats.length).toBeGreaterThan(0);
expect(data.hotspots.length).toBeGreaterThan(0);
expect(data.hotspots[0].name).toBe('TestSystem');
});
test('should calculate percentages correctly', () => {
const mockMonitor = {
getAllSystemStats: () => new Map([
['System1', { averageTime: 10, executionCount: 1 }],
['System2', { averageTime: 20, executionCount: 1 }]
]),
getAllSystemData: () => new Map([
['System1', { executionTime: 10 }],
['System2', { executionTime: 20 }]
])
};
const data = collector.collectFromLegacyMonitor(mockMonitor);
// Check that percentages are calculated
const ecsCat = data.categoryStats.find(c => c.category === 'ECS');
expect(ecsCat).toBeDefined();
expect(ecsCat!.totalTime).toBe(30);
});
test('should handle empty stats', () => {
const mockMonitor = {
getAllSystemStats: () => new Map(),
getAllSystemData: () => new Map()
};
const data = collector.collectFromLegacyMonitor(mockMonitor);
expect(data.categoryStats).toEqual([]);
expect(data.hotspots).toEqual([]);
});
});
describe('IAdvancedProfilerData structure', () => {
test('should have all required fields', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
// Verify structure
expect(data).toHaveProperty('currentFrame');
expect(data).toHaveProperty('frameTimeHistory');
expect(data).toHaveProperty('categoryStats');
expect(data).toHaveProperty('hotspots');
expect(data).toHaveProperty('callGraph');
expect(data).toHaveProperty('longTasks');
expect(data).toHaveProperty('memoryTrend');
expect(data).toHaveProperty('summary');
// Verify currentFrame structure
expect(data.currentFrame).toHaveProperty('frameNumber');
expect(data.currentFrame).toHaveProperty('frameTime');
expect(data.currentFrame).toHaveProperty('fps');
expect(data.currentFrame).toHaveProperty('memory');
// Verify callGraph structure
expect(data.callGraph).toHaveProperty('currentFunction');
expect(data.callGraph).toHaveProperty('callers');
expect(data.callGraph).toHaveProperty('callees');
// Verify summary structure
expect(data.summary).toHaveProperty('totalFrames');
expect(data.summary).toHaveProperty('averageFrameTime');
expect(data.summary).toHaveProperty('minFrameTime');
expect(data.summary).toHaveProperty('maxFrameTime');
expect(data.summary).toHaveProperty('p95FrameTime');
expect(data.summary).toHaveProperty('p99FrameTime');
expect(data.summary).toHaveProperty('currentMemoryMB');
expect(data.summary).toHaveProperty('peakMemoryMB');
expect(data.summary).toHaveProperty('gcCount');
expect(data.summary).toHaveProperty('longTaskCount');
});
test('hotspot items should have correct structure', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.Script);
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
const hotspot = data.hotspots[0];
if (hotspot) {
expect(hotspot).toHaveProperty('name');
expect(hotspot).toHaveProperty('category');
expect(hotspot).toHaveProperty('inclusiveTime');
expect(hotspot).toHaveProperty('inclusiveTimePercent');
expect(hotspot).toHaveProperty('exclusiveTime');
expect(hotspot).toHaveProperty('exclusiveTimePercent');
expect(hotspot).toHaveProperty('callCount');
expect(hotspot).toHaveProperty('avgCallTime');
}
});
test('category stats items should have correct structure', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.ECS);
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
const category = data.categoryStats[0];
if (category) {
expect(category).toHaveProperty('category');
expect(category).toHaveProperty('totalTime');
expect(category).toHaveProperty('percentOfFrame');
expect(category).toHaveProperty('sampleCount');
expect(category).toHaveProperty('items');
}
});
});
describe('Edge cases', () => {
test('should handle no profiler data', () => {
ProfilerSDK.reset();
const data = collector.collectAdvancedData();
expect(data).toBeDefined();
expect(data.currentFrame.frameNumber).toBe(0);
});
test('should track peak memory', () => {
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
collector.collectAdvancedData();
ProfilerSDK.beginFrame();
ProfilerSDK.endFrame();
const data = collector.collectAdvancedData();
// Peak should be maintained or increased
expect(data.summary.peakMemoryMB).toBeGreaterThanOrEqual(0);
});
test('should handle multiple frames with varying data', () => {
for (let i = 0; i < 10; i++) {
ProfilerSDK.beginFrame();
if (i % 2 === 0) {
ProfilerSDK.measure('EvenFrame', () => {}, ProfileCategory.ECS);
} else {
ProfilerSDK.measure('OddFrame', () => {}, ProfileCategory.Rendering);
}
ProfilerSDK.endFrame();
}
const data = collector.collectAdvancedData();
expect(data.frameTimeHistory.length).toBe(10);
expect(data.summary.totalFrames).toBe(10);
});
});
});

View File

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

View File

@@ -36,11 +36,10 @@ describe('Timer - 定时器测试', () => {
it('应该能够初始化定时器', () => {
timer.initialize(1.0, false, mockContext, mockCallback);
expect(timer.context).toBe(mockContext);
expect(timer._timeInSeconds).toBe(1.0);
expect(timer._repeats).toBe(false);
expect(timer._onTime).toBeDefined();
expect(timer.isDone).toBe(false);
expect(timer.elapsedTime).toBe(0);
});
it('应该能够获取泛型上下文', () => {
@@ -190,11 +189,10 @@ describe('Timer - 定时器测试', () => {
describe('内存管理', () => {
it('unload应该清空对象引用', () => {
timer.initialize(1.0, false, mockContext, mockCallback);
timer.unload();
expect(timer.context).toBeNull();
expect(timer._onTime).toBeNull();
});
});