refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
104
packages/framework/core/src/Utils/BinarySerializer.ts
Normal file
104
packages/framework/core/src/Utils/BinarySerializer.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 二进制序列化器
|
||||
* 将对象转换为UTF8字节数组
|
||||
*/
|
||||
export class BinarySerializer {
|
||||
/**
|
||||
* 将字符串编码为UTF8字节数组
|
||||
*/
|
||||
private static stringToUtf8(str: string): Uint8Array {
|
||||
const len = str.length;
|
||||
let pos = 0;
|
||||
const bytes: number[] = [];
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
let code = str.charCodeAt(i);
|
||||
|
||||
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < len) {
|
||||
const high = code;
|
||||
const low = str.charCodeAt(i + 1);
|
||||
if (low >= 0xDC00 && low <= 0xDFFF) {
|
||||
code = 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (code < 0x80) {
|
||||
bytes[pos++] = code;
|
||||
} else if (code < 0x800) {
|
||||
bytes[pos++] = 0xC0 | (code >> 6);
|
||||
bytes[pos++] = 0x80 | (code & 0x3F);
|
||||
} else if (code < 0x10000) {
|
||||
bytes[pos++] = 0xE0 | (code >> 12);
|
||||
bytes[pos++] = 0x80 | ((code >> 6) & 0x3F);
|
||||
bytes[pos++] = 0x80 | (code & 0x3F);
|
||||
} else {
|
||||
bytes[pos++] = 0xF0 | (code >> 18);
|
||||
bytes[pos++] = 0x80 | ((code >> 12) & 0x3F);
|
||||
bytes[pos++] = 0x80 | ((code >> 6) & 0x3F);
|
||||
bytes[pos++] = 0x80 | (code & 0x3F);
|
||||
}
|
||||
}
|
||||
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将UTF8字节数组解码为字符串
|
||||
*/
|
||||
private static utf8ToString(bytes: Uint8Array): string {
|
||||
const len = bytes.length;
|
||||
let str = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < len) {
|
||||
const byte1 = bytes[i++];
|
||||
if (byte1 === undefined) break;
|
||||
|
||||
if (byte1 < 0x80) {
|
||||
str += String.fromCharCode(byte1);
|
||||
} else if ((byte1 & 0xE0) === 0xC0) {
|
||||
const byte2 = bytes[i++];
|
||||
if (byte2 === undefined) break;
|
||||
str += String.fromCharCode(((byte1 & 0x1F) << 6) | (byte2 & 0x3F));
|
||||
} else if ((byte1 & 0xF0) === 0xE0) {
|
||||
const byte2 = bytes[i++];
|
||||
const byte3 = bytes[i++];
|
||||
if (byte2 === undefined || byte3 === undefined) break;
|
||||
str += String.fromCharCode(
|
||||
((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F)
|
||||
);
|
||||
} else if ((byte1 & 0xF8) === 0xF0) {
|
||||
const byte2 = bytes[i++];
|
||||
const byte3 = bytes[i++];
|
||||
const byte4 = bytes[i++];
|
||||
if (byte2 === undefined || byte3 === undefined || byte4 === undefined) break;
|
||||
let code = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) |
|
||||
((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
|
||||
code -= 0x10000;
|
||||
str += String.fromCharCode(
|
||||
0xD800 + (code >> 10),
|
||||
0xDC00 + (code & 0x3FF)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象编码为二进制数据
|
||||
*/
|
||||
public static encode(value: any): Uint8Array {
|
||||
const jsonString = JSON.stringify(value);
|
||||
return this.stringToUtf8(jsonString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将二进制数据解码为对象
|
||||
*/
|
||||
public static decode(bytes: Uint8Array): any {
|
||||
const jsonString = this.utf8ToString(bytes);
|
||||
return JSON.parse(jsonString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* 高级性能分析数据收集器
|
||||
*
|
||||
* 整合 ProfilerSDK 和现有 PerformanceMonitor 的数据,
|
||||
* 提供统一的高级性能分析数据接口
|
||||
*/
|
||||
|
||||
import { ProfilerSDK } from '../Profiler/ProfilerSDK';
|
||||
import {
|
||||
ProfileCategory,
|
||||
ProfileFrame,
|
||||
ProfileReport,
|
||||
MemorySnapshot
|
||||
} from '../Profiler/ProfilerTypes';
|
||||
import { Time } from '../Time';
|
||||
|
||||
/**
|
||||
* 旧版 PerformanceMonitor 接口 (用于兼容)
|
||||
*/
|
||||
export type ILegacyPerformanceMonitor = {
|
||||
getAllSystemStats?: () => Map<string, {
|
||||
averageTime: number;
|
||||
minTime?: number;
|
||||
maxTime?: number;
|
||||
executionCount?: number;
|
||||
}>;
|
||||
getAllSystemData?: () => Map<string, {
|
||||
executionTime: number;
|
||||
entityCount?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 热点函数项(支持递归层级)
|
||||
*/
|
||||
export type IHotspotItem = {
|
||||
name: string;
|
||||
category: string;
|
||||
inclusiveTime: number;
|
||||
inclusiveTimePercent: number;
|
||||
exclusiveTime: number;
|
||||
exclusiveTimePercent: number;
|
||||
callCount: number;
|
||||
avgCallTime: number;
|
||||
/** 层级深度 */
|
||||
depth: number;
|
||||
/** 子函数 */
|
||||
children?: IHotspotItem[] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级性能数据接口
|
||||
*/
|
||||
export type 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: IHotspotItem[];
|
||||
/** 调用关系数据 */
|
||||
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;
|
||||
|
||||
// 使用 callGraph 构建层级结构
|
||||
// 找出所有根节点(没有被任何函数调用的,或者是顶层函数)
|
||||
const rootFunctions = new Set<string>();
|
||||
const childFunctions = new Set<string>();
|
||||
|
||||
for (const [name, node] of report.callGraph) {
|
||||
// 如果没有调用者,或者调用者不在 hotspots 中,则是根节点
|
||||
if (node.callers.size === 0) {
|
||||
rootFunctions.add(name);
|
||||
} else {
|
||||
// 检查是否所有调用者都在 hotspots 之外
|
||||
let hasParentInHotspots = false;
|
||||
for (const callerName of node.callers.keys()) {
|
||||
if (report.callGraph.has(callerName)) {
|
||||
hasParentInHotspots = true;
|
||||
childFunctions.add(name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasParentInHotspots) {
|
||||
rootFunctions.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归构建层级热点数据
|
||||
const buildHotspotItem = (
|
||||
name: string,
|
||||
depth: number,
|
||||
visited: Set<string>
|
||||
): IHotspotItem | null => {
|
||||
if (visited.has(name)) return null; // 避免循环
|
||||
visited.add(name);
|
||||
|
||||
const stats = report.hotspots.find(h => h.name === name);
|
||||
const node = report.callGraph.get(name);
|
||||
|
||||
if (!stats && !node) return null;
|
||||
|
||||
const inclusiveTime = stats?.inclusiveTime || node?.totalTime || 0;
|
||||
const exclusiveTime = stats?.exclusiveTime || inclusiveTime;
|
||||
const callCount = stats?.callCount || node?.callCount || 1;
|
||||
|
||||
// 构建子节点
|
||||
const children: IHotspotItem[] = [];
|
||||
if (node && depth < 5) { // 限制深度避免过深
|
||||
for (const [calleeName] of node.callees) {
|
||||
const child = buildHotspotItem(calleeName, depth + 1, visited);
|
||||
if (child) {
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
// 按耗时排序
|
||||
children.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
category: stats?.category || node?.category || ProfileCategory.Custom,
|
||||
inclusiveTime,
|
||||
inclusiveTimePercent: (inclusiveTime / totalTime) * 100,
|
||||
exclusiveTime,
|
||||
exclusiveTimePercent: (exclusiveTime / totalTime) * 100,
|
||||
callCount,
|
||||
avgCallTime: callCount > 0 ? inclusiveTime / callCount : 0,
|
||||
depth,
|
||||
children: children.length > 0 ? children : undefined
|
||||
};
|
||||
};
|
||||
|
||||
// 构建根节点列表
|
||||
const result: IAdvancedProfilerData['hotspots'] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
for (const rootName of rootFunctions) {
|
||||
const item = buildHotspotItem(rootName, 0, visited);
|
||||
if (item) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 按耗时排序
|
||||
result.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
|
||||
|
||||
return result.slice(0, 50);
|
||||
}
|
||||
|
||||
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,
|
||||
depth: 0
|
||||
});
|
||||
}
|
||||
|
||||
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: []
|
||||
};
|
||||
}
|
||||
|
||||
// 计算所有调用者的总调用次数(用于计算调用者的百分比)
|
||||
let totalCallerCount = 0;
|
||||
for (const data of node.callers.values()) {
|
||||
totalCallerCount += data.count;
|
||||
}
|
||||
|
||||
// Calling Functions(谁调用了我)
|
||||
// - totalTime: 该调用者调用当前函数时的平均耗时
|
||||
// - percentOfCurrent: 该调用者的调用次数占总调用次数的百分比
|
||||
const callers = Array.from(node.callers.entries())
|
||||
.map(([name, data]) => ({
|
||||
name,
|
||||
callCount: data.count,
|
||||
totalTime: data.totalTime,
|
||||
percentOfCurrent: totalCallerCount > 0 ? (data.count / totalCallerCount) * 100 : 0
|
||||
}))
|
||||
.sort((a, b) => b.callCount - a.callCount);
|
||||
|
||||
// Called Functions(我调用了谁)
|
||||
// - totalTime: 当前函数调用该被调用者时的平均耗时
|
||||
// - percentOfCurrent: 该被调用者的耗时占当前函数耗时的百分比
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import { IComponentDebugData } from '../../Types';
|
||||
import { ComponentPoolManager } from '../../ECS/Core/ComponentPool';
|
||||
import { getComponentInstanceTypeName } from '../../ECS/Decorators';
|
||||
import { IScene } from '../../ECS/IScene';
|
||||
|
||||
/**
|
||||
* 组件数据收集器
|
||||
*/
|
||||
export class ComponentDataCollector {
|
||||
private static componentSizeCache = new Map<string, number>();
|
||||
|
||||
/**
|
||||
* 收集组件数据(轻量版,不计算实际内存大小)
|
||||
* @param scene 场景实例
|
||||
*/
|
||||
public collectComponentData(scene?: IScene | null): IComponentDebugData {
|
||||
if (!scene) {
|
||||
return {
|
||||
componentTypes: 0,
|
||||
componentInstances: 0,
|
||||
componentStats: []
|
||||
};
|
||||
}
|
||||
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) {
|
||||
return {
|
||||
componentTypes: 0,
|
||||
componentInstances: 0,
|
||||
componentStats: []
|
||||
};
|
||||
}
|
||||
|
||||
const componentStats = new Map<string, { count: number; entities: number }>();
|
||||
let totalInstances = 0;
|
||||
|
||||
entityList.buffer.forEach((entity: any) => {
|
||||
if (entity.components) {
|
||||
entity.components.forEach((component: any) => {
|
||||
const typeName = getComponentInstanceTypeName(component);
|
||||
const stats = componentStats.get(typeName) || { count: 0, entities: 0 };
|
||||
stats.count++;
|
||||
totalInstances++;
|
||||
componentStats.set(typeName, stats);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取池利用率信息
|
||||
const poolUtilizations = new Map<string, number>();
|
||||
const poolSizes = new Map<string, number>();
|
||||
|
||||
try {
|
||||
const poolManager = ComponentPoolManager.getInstance();
|
||||
const poolStats = poolManager.getPoolStats();
|
||||
const utilizations = poolManager.getPoolUtilization();
|
||||
|
||||
for (const [typeName, stats] of poolStats.entries()) {
|
||||
poolSizes.set(typeName, stats.maxSize);
|
||||
}
|
||||
|
||||
for (const [typeName, util] of utilizations.entries()) {
|
||||
poolUtilizations.set(typeName, util.utilization);
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果无法获取池信息,使用默认值
|
||||
}
|
||||
|
||||
return {
|
||||
componentTypes: componentStats.size,
|
||||
componentInstances: totalInstances,
|
||||
componentStats: Array.from(componentStats.entries()).map(([typeName, stats]) => {
|
||||
const poolSize = poolSizes.get(typeName) || 0;
|
||||
const poolUtilization = poolUtilizations.get(typeName) || 0;
|
||||
// 使用预估的基础内存大小,避免每帧计算
|
||||
const memoryPerInstance = this.getEstimatedComponentSize(typeName, scene);
|
||||
|
||||
return {
|
||||
typeName,
|
||||
instanceCount: stats.count,
|
||||
memoryPerInstance: memoryPerInstance,
|
||||
totalMemory: stats.count * memoryPerInstance,
|
||||
poolSize: poolSize,
|
||||
poolUtilization: poolUtilization,
|
||||
averagePerEntity: stats.count / entityList.buffer.length
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件类型的估算内存大小(基于预设值,不进行实际计算)
|
||||
*/
|
||||
private getEstimatedComponentSize(typeName: string, scene?: IScene | null): number {
|
||||
if (ComponentDataCollector.componentSizeCache.has(typeName)) {
|
||||
return ComponentDataCollector.componentSizeCache.get(typeName)!;
|
||||
}
|
||||
|
||||
if (!scene) return 64;
|
||||
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) return 64;
|
||||
|
||||
let calculatedSize = 64;
|
||||
|
||||
try {
|
||||
for (const entity of entityList.buffer) {
|
||||
if (entity.components) {
|
||||
const component = entity.components.find((c: any) => getComponentInstanceTypeName(c) === typeName);
|
||||
if (component) {
|
||||
calculatedSize = this.calculateQuickObjectSize(component);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
calculatedSize = 64;
|
||||
}
|
||||
|
||||
ComponentDataCollector.componentSizeCache.set(typeName, calculatedSize);
|
||||
return calculatedSize;
|
||||
}
|
||||
|
||||
private calculateQuickObjectSize(obj: any): number {
|
||||
if (!obj || typeof obj !== 'object') return 8;
|
||||
|
||||
let size = 32;
|
||||
const visited = new WeakSet();
|
||||
|
||||
const calculate = (item: any, depth: number = 0): number => {
|
||||
if (!item || typeof item !== 'object' || visited.has(item) || depth > 3) {
|
||||
return 0;
|
||||
}
|
||||
visited.add(item);
|
||||
|
||||
let itemSize = 0;
|
||||
|
||||
try {
|
||||
const keys = Object.keys(item);
|
||||
for (let i = 0; i < Math.min(keys.length, 20); i++) {
|
||||
const key = keys[i];
|
||||
if (!key || key === 'entity' || key === '_entity' || key === 'constructor') continue;
|
||||
|
||||
const value = item[key];
|
||||
itemSize += key.length * 2;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
itemSize += Math.min(value.length * 2, 200);
|
||||
} else if (typeof value === 'number') {
|
||||
itemSize += 8;
|
||||
} else if (typeof value === 'boolean') {
|
||||
itemSize += 4;
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
itemSize += calculate(value, depth + 1);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return 32;
|
||||
}
|
||||
|
||||
return itemSize;
|
||||
};
|
||||
|
||||
size += calculate(obj);
|
||||
return Math.max(size, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为内存快照功能提供的详细内存计算
|
||||
* 只在用户主动请求内存快照时调用
|
||||
* @param typeName 组件类型名称
|
||||
* @param scene 场景实例
|
||||
*/
|
||||
public calculateDetailedComponentMemory(typeName: string, scene?: IScene | null): number {
|
||||
if (!scene) return this.getEstimatedComponentSize(typeName, scene);
|
||||
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) return this.getEstimatedComponentSize(typeName, scene);
|
||||
|
||||
try {
|
||||
// 找到第一个包含此组件的实体,分析组件大小
|
||||
for (const entity of entityList.buffer) {
|
||||
if (entity.components) {
|
||||
const component = entity.components.find((c: any) => getComponentInstanceTypeName(c) === typeName);
|
||||
if (component) {
|
||||
return this.estimateObjectSize(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误,使用估算值
|
||||
}
|
||||
|
||||
return this.getEstimatedComponentSize(typeName, scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算对象内存大小(仅用于内存快照)
|
||||
* 优化版本:减少递归深度,提高性能
|
||||
*/
|
||||
private estimateObjectSize(obj: any, visited = new WeakSet(), depth = 0): number {
|
||||
if (obj === null || obj === undefined || depth > 10) return 0;
|
||||
if (visited.has(obj)) return 0;
|
||||
|
||||
let size = 0;
|
||||
const type = typeof obj;
|
||||
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
size = 4;
|
||||
break;
|
||||
case 'number':
|
||||
size = 8;
|
||||
break;
|
||||
case 'string':
|
||||
size = 24 + Math.min(obj.length * 2, 1000);
|
||||
break;
|
||||
case 'object':
|
||||
visited.add(obj);
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
size = 40 + (obj.length * 8);
|
||||
const maxElements = Math.min(obj.length, 50);
|
||||
for (let i = 0; i < maxElements; i++) {
|
||||
size += this.estimateObjectSize(obj[i], visited, depth + 1);
|
||||
}
|
||||
} else {
|
||||
size = 32;
|
||||
|
||||
try {
|
||||
const ownKeys = Object.getOwnPropertyNames(obj);
|
||||
const maxProps = Math.min(ownKeys.length, 30);
|
||||
|
||||
for (let i = 0; i < maxProps; i++) {
|
||||
const key = ownKeys[i];
|
||||
if (!key) continue;
|
||||
|
||||
if (key === 'constructor' ||
|
||||
key === '__proto__' ||
|
||||
key === 'entity' ||
|
||||
key === '_entity' ||
|
||||
key.startsWith('_cc_') ||
|
||||
key.startsWith('__')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
size += 16 + (key.length * 2);
|
||||
|
||||
const value = obj[key];
|
||||
if (value !== undefined && value !== null) {
|
||||
size += this.estimateObjectSize(value, visited, depth + 1);
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
size = 128;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
size = 8;
|
||||
}
|
||||
|
||||
return Math.ceil(size / 8) * 8;
|
||||
}
|
||||
|
||||
public static clearCache(): void {
|
||||
ComponentDataCollector.componentSizeCache.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IECSDebugConfig } from '../../Types';
|
||||
import { Injectable } from '../../Core/DI/Decorators';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 调试配置服务
|
||||
*
|
||||
* 管理调试系统的配置信息
|
||||
*/
|
||||
@Injectable()
|
||||
export class DebugConfigService implements IService {
|
||||
private _config: IECSDebugConfig;
|
||||
|
||||
constructor() {
|
||||
this._config = {
|
||||
enabled: false,
|
||||
websocketUrl: '',
|
||||
debugFrameRate: 30,
|
||||
autoReconnect: true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: IECSDebugConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getConfig(): IECSDebugConfig {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return this._config.enabled;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
1034
packages/framework/core/src/Utils/Debug/DebugManager.ts
Normal file
1034
packages/framework/core/src/Utils/Debug/DebugManager.ts
Normal file
File diff suppressed because it is too large
Load Diff
1047
packages/framework/core/src/Utils/Debug/EntityDataCollector.ts
Normal file
1047
packages/framework/core/src/Utils/Debug/EntityDataCollector.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,222 @@
|
||||
import { IPerformanceDebugData } from '../../Types';
|
||||
import { Time } from '../Time';
|
||||
|
||||
/**
|
||||
* 性能数据收集器
|
||||
*/
|
||||
export class PerformanceDataCollector {
|
||||
private frameTimeHistory: number[] = [];
|
||||
private maxHistoryLength: number = 60;
|
||||
private gcCollections: number = 0;
|
||||
private lastMemoryCheck: number = 0;
|
||||
|
||||
/**
|
||||
* 收集性能数据
|
||||
*/
|
||||
public collectPerformanceData(performanceMonitor: any): IPerformanceDebugData {
|
||||
const frameTimeSeconds = Time.deltaTime;
|
||||
const engineFrameTimeMs = frameTimeSeconds * 1000;
|
||||
const currentFps = frameTimeSeconds > 0 ? Math.round(1 / frameTimeSeconds) : 0;
|
||||
|
||||
const ecsPerformanceData = this.getECSPerformanceData(performanceMonitor);
|
||||
const ecsExecutionTimeMs = ecsPerformanceData.totalExecutionTime;
|
||||
const ecsPercentage = engineFrameTimeMs > 0 ? (ecsExecutionTimeMs / engineFrameTimeMs * 100) : 0;
|
||||
|
||||
let memoryUsage = 0;
|
||||
if ((performance as any).memory) {
|
||||
memoryUsage = (performance as any).memory.usedJSHeapSize / 1024 / 1024;
|
||||
}
|
||||
|
||||
// 更新ECS执行时间历史记录
|
||||
this.frameTimeHistory.push(ecsExecutionTimeMs);
|
||||
if (this.frameTimeHistory.length > this.maxHistoryLength) {
|
||||
this.frameTimeHistory.shift();
|
||||
}
|
||||
|
||||
// 计算ECS执行时间统计
|
||||
const history = this.frameTimeHistory.filter((t) => t >= 0);
|
||||
const averageECSTime = history.length > 0 ? history.reduce((a, b) => a + b, 0) / history.length : ecsExecutionTimeMs;
|
||||
const minECSTime = history.length > 0 ? Math.min(...history) : ecsExecutionTimeMs;
|
||||
const maxECSTime = history.length > 0 ? Math.max(...history) : ecsExecutionTimeMs;
|
||||
|
||||
return {
|
||||
frameTime: ecsExecutionTimeMs,
|
||||
engineFrameTime: engineFrameTimeMs,
|
||||
ecsPercentage: ecsPercentage,
|
||||
memoryUsage: memoryUsage,
|
||||
fps: currentFps,
|
||||
averageFrameTime: averageECSTime,
|
||||
minFrameTime: minECSTime,
|
||||
maxFrameTime: maxECSTime,
|
||||
frameTimeHistory: [...this.frameTimeHistory],
|
||||
systemPerformance: this.getSystemPerformance(performanceMonitor),
|
||||
systemBreakdown: ecsPerformanceData.systemBreakdown,
|
||||
memoryDetails: this.getMemoryDetails()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ECS框架整体性能数据
|
||||
*/
|
||||
private getECSPerformanceData(performanceMonitor: any): { totalExecutionTime: number; systemBreakdown: Array<any> } {
|
||||
// 检查性能监视器是否存在
|
||||
if (!performanceMonitor) {
|
||||
return { totalExecutionTime: 0, systemBreakdown: [] };
|
||||
}
|
||||
|
||||
if (!performanceMonitor.enabled) {
|
||||
// 尝试启用性能监视器
|
||||
try {
|
||||
performanceMonitor.enabled = true;
|
||||
} catch (error) {
|
||||
// 如果无法启用,返回默认值
|
||||
}
|
||||
return { totalExecutionTime: 0, systemBreakdown: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
let totalTime = 0;
|
||||
const systemBreakdown = [];
|
||||
|
||||
const stats = performanceMonitor.getAllSystemStats();
|
||||
|
||||
if (stats.size === 0) {
|
||||
return { totalExecutionTime: 0, systemBreakdown: [] };
|
||||
}
|
||||
|
||||
// 计算各系统的执行时间
|
||||
for (const [systemName, stat] of stats.entries()) {
|
||||
// 使用最近的执行时间而不是平均时间,这样更能反映当前状态
|
||||
const systemTime = stat.recentTimes && stat.recentTimes.length > 0 ?
|
||||
stat.recentTimes[stat.recentTimes.length - 1] :
|
||||
(stat.averageTime || 0);
|
||||
|
||||
totalTime += systemTime;
|
||||
systemBreakdown.push({
|
||||
systemName: systemName,
|
||||
executionTime: systemTime,
|
||||
percentage: 0 // 后面计算
|
||||
});
|
||||
}
|
||||
|
||||
// 计算各系统占ECS总时间的百分比
|
||||
systemBreakdown.forEach((system) => {
|
||||
system.percentage = totalTime > 0 ? (system.executionTime / totalTime * 100) : 0;
|
||||
});
|
||||
|
||||
// 按执行时间排序
|
||||
systemBreakdown.sort((a, b) => b.executionTime - a.executionTime);
|
||||
|
||||
return {
|
||||
totalExecutionTime: totalTime,
|
||||
systemBreakdown: systemBreakdown
|
||||
};
|
||||
} catch (error) {
|
||||
return { totalExecutionTime: 0, systemBreakdown: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统性能数据
|
||||
*/
|
||||
private getSystemPerformance(performanceMonitor: any): Array<any> {
|
||||
if (!performanceMonitor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = performanceMonitor.getAllSystemStats();
|
||||
const systemData = performanceMonitor.getAllSystemData();
|
||||
|
||||
return Array.from(stats.entries() as Iterable<[string, any]>).map(([systemName, stat]) => {
|
||||
const data = systemData.get(systemName);
|
||||
return {
|
||||
systemName: systemName,
|
||||
averageTime: stat.averageTime || 0,
|
||||
maxTime: stat.maxTime || 0,
|
||||
minTime: stat.minTime === Number.MAX_VALUE ? 0 : (stat.minTime || 0),
|
||||
samples: stat.executionCount || 0,
|
||||
percentage: 0,
|
||||
entityCount: data?.entityCount || 0,
|
||||
lastExecutionTime: data?.executionTime || 0
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存详情
|
||||
*/
|
||||
private getMemoryDetails(): any {
|
||||
const memoryInfo: any = {
|
||||
entities: 0,
|
||||
components: 0,
|
||||
systems: 0,
|
||||
pooled: 0,
|
||||
totalMemory: 0,
|
||||
usedMemory: 0,
|
||||
freeMemory: 0,
|
||||
gcCollections: this.updateGCCount()
|
||||
};
|
||||
|
||||
try {
|
||||
if ((performance as any).memory) {
|
||||
const perfMemory = (performance as any).memory;
|
||||
memoryInfo.totalMemory = perfMemory.jsHeapSizeLimit || 512 * 1024 * 1024;
|
||||
memoryInfo.usedMemory = perfMemory.usedJSHeapSize || 0;
|
||||
memoryInfo.freeMemory = memoryInfo.totalMemory - memoryInfo.usedMemory;
|
||||
|
||||
// 检测GC:如果使用的内存突然大幅减少,可能发生了GC
|
||||
if (this.lastMemoryCheck > 0) {
|
||||
const memoryDrop = this.lastMemoryCheck - memoryInfo.usedMemory;
|
||||
if (memoryDrop > 1024 * 1024) { // 内存减少超过1MB
|
||||
this.gcCollections++;
|
||||
}
|
||||
}
|
||||
this.lastMemoryCheck = memoryInfo.usedMemory;
|
||||
} else {
|
||||
memoryInfo.totalMemory = 512 * 1024 * 1024;
|
||||
memoryInfo.freeMemory = 512 * 1024 * 1024;
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
totalMemory: 0,
|
||||
usedMemory: 0,
|
||||
freeMemory: 0,
|
||||
entityMemory: 0,
|
||||
componentMemory: 0,
|
||||
systemMemory: 0,
|
||||
pooledMemory: 0,
|
||||
gcCollections: this.gcCollections
|
||||
};
|
||||
}
|
||||
|
||||
return memoryInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新GC计数
|
||||
*/
|
||||
private updateGCCount(): number {
|
||||
try {
|
||||
// 尝试使用PerformanceObserver来检测GC
|
||||
if (typeof PerformanceObserver !== 'undefined') {
|
||||
// 这是一个简化的GC检测方法
|
||||
// 实际的GC检测需要更复杂的逻辑
|
||||
return this.gcCollections;
|
||||
}
|
||||
|
||||
// 如果有其他GC检测API,可以在这里添加
|
||||
if ((performance as any).measureUserAgentSpecificMemory) {
|
||||
// 实验性API,可能不可用
|
||||
return this.gcCollections;
|
||||
}
|
||||
|
||||
return this.gcCollections;
|
||||
} catch (error) {
|
||||
return this.gcCollections;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ISceneDebugData } from '../../Types';
|
||||
import { IScene } from '../../ECS/IScene';
|
||||
|
||||
/**
|
||||
* 场景数据收集器
|
||||
* Scene data collector
|
||||
*
|
||||
* 收集场景的调试信息,通过公共接口访问数据。
|
||||
* Collects scene debug information through public interfaces.
|
||||
*/
|
||||
export class SceneDataCollector {
|
||||
private sceneStartTime: number = Date.now();
|
||||
|
||||
/**
|
||||
* 收集场景数据
|
||||
* Collect scene data
|
||||
*
|
||||
* @param scene 场景实例 | Scene instance
|
||||
*/
|
||||
public collectSceneData(scene?: IScene | null): ISceneDebugData {
|
||||
if (!scene) {
|
||||
return {
|
||||
currentSceneName: 'No Scene',
|
||||
isInitialized: false,
|
||||
sceneRunTime: 0,
|
||||
sceneEntityCount: 0,
|
||||
sceneSystemCount: 0,
|
||||
sceneUptime: 0
|
||||
};
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
const runTime = (currentTime - this.sceneStartTime) / 1000;
|
||||
|
||||
// 使用公共接口获取数据 | Use public interface to get data
|
||||
const stats = scene.getStats();
|
||||
|
||||
return {
|
||||
currentSceneName: scene.name || 'Unnamed Scene',
|
||||
isInitialized: true, // 如果 scene 存在,则认为已初始化 | If scene exists, consider initialized
|
||||
sceneRunTime: runTime,
|
||||
sceneEntityCount: stats.entityCount,
|
||||
sceneSystemCount: stats.processorCount,
|
||||
sceneUptime: runTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置场景开始时间
|
||||
*/
|
||||
public setSceneStartTime(time: number): void {
|
||||
this.sceneStartTime = time;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ISystemDebugData } from '../../Types';
|
||||
import { getSystemInstanceTypeName } from '../../ECS/Decorators';
|
||||
import { IScene } from '../../ECS/IScene';
|
||||
|
||||
/**
|
||||
* 系统数据收集器
|
||||
* System data collector
|
||||
*
|
||||
* 收集系统的调试信息,通过公共接口访问数据。
|
||||
* Collects system debug information through public interfaces.
|
||||
*/
|
||||
export class SystemDataCollector {
|
||||
/**
|
||||
* 收集系统数据
|
||||
* Collect system data
|
||||
*
|
||||
* @param performanceMonitor 性能监视器实例 | Performance monitor instance
|
||||
* @param scene 场景实例 | Scene instance
|
||||
*/
|
||||
public collectSystemData(performanceMonitor: any, scene?: IScene | null): ISystemDebugData {
|
||||
if (!scene) {
|
||||
return {
|
||||
totalSystems: 0,
|
||||
systemsInfo: []
|
||||
};
|
||||
}
|
||||
|
||||
// 使用公共接口 | Use public interface
|
||||
const systems = scene.systems || [];
|
||||
|
||||
// 获取性能监控数据
|
||||
let systemStats: Map<string, any> = new Map();
|
||||
let systemData: Map<string, any> = new Map();
|
||||
|
||||
if (performanceMonitor) {
|
||||
try {
|
||||
systemStats = performanceMonitor.getAllSystemStats();
|
||||
systemData = performanceMonitor.getAllSystemData();
|
||||
} catch (error) {
|
||||
// 忽略错误,使用空的Map
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSystems: systems.length,
|
||||
systemsInfo: systems.map((system: any) => {
|
||||
const systemName = system.systemName || getSystemInstanceTypeName(system);
|
||||
const stats = systemStats.get(systemName);
|
||||
const data = systemData.get(systemName);
|
||||
|
||||
return {
|
||||
name: systemName,
|
||||
type: getSystemInstanceTypeName(system),
|
||||
entityCount: system.entities?.length || 0,
|
||||
executionTime: stats?.averageTime || data?.executionTime || 0,
|
||||
minExecutionTime: stats?.minTime === Number.MAX_VALUE ? 0 : (stats?.minTime || 0),
|
||||
maxExecutionTime: stats?.maxTime || 0,
|
||||
executionTimeHistory: stats?.recentTimes || [],
|
||||
updateOrder: system.updateOrder || 0,
|
||||
enabled: system.enabled !== false,
|
||||
lastUpdateTime: data?.lastUpdateTime || 0
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
169
packages/framework/core/src/Utils/Debug/WebSocketManager.ts
Normal file
169
packages/framework/core/src/Utils/Debug/WebSocketManager.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* WebSocket连接管理器
|
||||
*/
|
||||
export class WebSocketManager {
|
||||
private ws?: WebSocket;
|
||||
private isConnected: boolean = false;
|
||||
private reconnectAttempts: number = 0;
|
||||
private maxReconnectAttempts: number = 5;
|
||||
private url: string;
|
||||
private autoReconnect: boolean;
|
||||
private reconnectTimer?: ReturnType<typeof setTimeout>;
|
||||
private onOpen?: (event: Event) => void;
|
||||
private onClose?: (event: CloseEvent) => void;
|
||||
private onError?: (error: Event | any) => void;
|
||||
private messageHandler?: (message: any) => void;
|
||||
|
||||
constructor(url: string, autoReconnect: boolean = true) {
|
||||
this.url = url;
|
||||
this.autoReconnect = autoReconnect;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置消息处理回调
|
||||
*/
|
||||
public setMessageHandler(handler: (message: any) => void): void {
|
||||
this.messageHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接WebSocket
|
||||
*/
|
||||
public connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onopen = (event) => {
|
||||
this.handleOpen(event);
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
this.handleClose(event);
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
this.handleError(error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.handleConnectionFailure(error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
public disconnect(): void {
|
||||
if (this.ws) {
|
||||
this.autoReconnect = false; // 主动断开时不自动重连
|
||||
this.ws.close();
|
||||
delete (this as any).ws;
|
||||
}
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据
|
||||
*/
|
||||
public send(data: any): void {
|
||||
if (!this.isConnected || !this.ws) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
this.ws.send(message);
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
public getConnectionStatus(): boolean {
|
||||
return this.isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大重连次数
|
||||
*/
|
||||
public setMaxReconnectAttempts(attempts: number): void {
|
||||
this.maxReconnectAttempts = attempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计划重连
|
||||
*/
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
}
|
||||
|
||||
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
||||
this.reconnectAttempts++;
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.connect().catch((_error) => {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*/
|
||||
private handleMessage(event: MessageEvent): void {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
// 调用消息处理回调
|
||||
if (this.messageHandler) {
|
||||
this.messageHandler(message);
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
private handleOpen(event: Event): void {
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
if (this.onOpen) {
|
||||
this.onOpen(event);
|
||||
}
|
||||
}
|
||||
|
||||
private handleClose(event: CloseEvent): void {
|
||||
this.isConnected = false;
|
||||
|
||||
if (this.onClose) {
|
||||
this.onClose(event);
|
||||
}
|
||||
|
||||
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: Event): void {
|
||||
if (this.onError) {
|
||||
this.onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleConnectionFailure(error: any): void {
|
||||
if (this.onError) {
|
||||
this.onError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
packages/framework/core/src/Utils/Debug/index.ts
Normal file
10
packages/framework/core/src/Utils/Debug/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { EntityDataCollector } from './EntityDataCollector';
|
||||
export { SystemDataCollector } from './SystemDataCollector';
|
||||
export { PerformanceDataCollector } from './PerformanceDataCollector';
|
||||
export { ComponentDataCollector } from './ComponentDataCollector';
|
||||
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';
|
||||
116
packages/framework/core/src/Utils/Emitter.ts
Normal file
116
packages/framework/core/src/Utils/Emitter.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 用于包装事件的一个小类
|
||||
*/
|
||||
export class FuncPack<TContext = unknown> {
|
||||
/** 函数 */
|
||||
public func: Function;
|
||||
/** 上下文 */
|
||||
public context: TContext;
|
||||
|
||||
constructor(func: Function, context: TContext) {
|
||||
this.func = func;
|
||||
this.context = context;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于事件管理
|
||||
*/
|
||||
export class Emitter<T, TContext = unknown> {
|
||||
private _messageTable: Map<T, FuncPack<TContext>[]>;
|
||||
|
||||
constructor() {
|
||||
this._messageTable = new Map<T, FuncPack<TContext>[]>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始监听项
|
||||
* @param eventType 监听类型
|
||||
* @param handler 监听函数
|
||||
* @param context 监听上下文
|
||||
*/
|
||||
public addObserver(eventType: T, handler: Function, context: TContext) {
|
||||
let list = this._messageTable.get(eventType);
|
||||
if (!list) {
|
||||
list = [];
|
||||
this._messageTable.set(eventType, list);
|
||||
}
|
||||
|
||||
if (!this.hasObserver(eventType, handler)) {
|
||||
list.push(new FuncPack<TContext>(handler, context));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除监听项
|
||||
* @param eventType 事件类型
|
||||
* @param handler 事件函数
|
||||
*/
|
||||
public removeObserver(eventType: T, handler: Function) {
|
||||
const messageData = this._messageTable.get(eventType);
|
||||
if (messageData) {
|
||||
const index = messageData.findIndex((data) => data.func == handler);
|
||||
if (index != -1)
|
||||
messageData.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发该事件
|
||||
* @param eventType 事件类型
|
||||
* @param data 事件数据
|
||||
*/
|
||||
public emit<TData = unknown>(eventType: T, ...data: TData[]) {
|
||||
const list = this._messageTable.get(eventType);
|
||||
if (list) {
|
||||
for (const observer of list) {
|
||||
observer.func.call(observer.context, ...data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在该类型的观察者
|
||||
* @param eventType 事件类型
|
||||
* @param handler 事件函数
|
||||
*/
|
||||
public hasObserver(eventType: T, handler: Function): boolean {
|
||||
const list = this._messageTable.get(eventType);
|
||||
return list ? list.some((observer) => observer.func === handler) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定事件类型的所有监听器
|
||||
* @param eventType 事件类型
|
||||
*/
|
||||
public removeAllObservers(eventType?: T): void {
|
||||
if (eventType !== undefined) {
|
||||
this._messageTable.delete(eventType);
|
||||
} else {
|
||||
this._messageTable.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放所有资源,清理所有监听器
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._messageTable.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件类型数量
|
||||
*/
|
||||
public getEventTypeCount(): number {
|
||||
return this._messageTable.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定事件类型的监听器数量
|
||||
* @param eventType 事件类型
|
||||
*/
|
||||
public getObserverCount(eventType: T): number {
|
||||
const list = this._messageTable.get(eventType);
|
||||
return list ? list.length : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 数字扩展工具类
|
||||
* 提供数字转换的实用方法
|
||||
*/
|
||||
export class NumberExtension {
|
||||
/**
|
||||
* 将值转换为数字
|
||||
* @param value 要转换的值
|
||||
* @returns 转换后的数字,如果值为undefined则返回0
|
||||
*/
|
||||
public static toNumber(value: unknown): number {
|
||||
if (value == undefined) return 0;
|
||||
return Number(value);
|
||||
}
|
||||
}
|
||||
14
packages/framework/core/src/Utils/Extensions/TypeUtils.ts
Normal file
14
packages/framework/core/src/Utils/Extensions/TypeUtils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 类型工具类
|
||||
* 提供类型相关的实用方法
|
||||
*/
|
||||
export class TypeUtils {
|
||||
/**
|
||||
* 获取对象的类型
|
||||
* @param obj 对象
|
||||
* @returns 对象的构造函数
|
||||
*/
|
||||
public static getType(obj: any) {
|
||||
return obj.constructor;
|
||||
}
|
||||
}
|
||||
3
packages/framework/core/src/Utils/Extensions/index.ts
Normal file
3
packages/framework/core/src/Utils/Extensions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 扩展工具类导出
|
||||
export { TypeUtils } from './TypeUtils';
|
||||
export { NumberExtension } from './NumberExtension';
|
||||
90
packages/framework/core/src/Utils/GUID.ts
Normal file
90
packages/framework/core/src/Utils/GUID.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* GUID 生成工具
|
||||
*
|
||||
* 提供跨平台的 UUID v4 生成功能,用于实体持久化标识。
|
||||
* 优先使用 crypto.randomUUID(),降级使用 Math.random() 实现。
|
||||
*
|
||||
* GUID generation utility.
|
||||
* Provides cross-platform UUID v4 generation for entity persistent identification.
|
||||
* Uses crypto.randomUUID() when available, falls back to Math.random() implementation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 生成 UUID v4 格式的 GUID
|
||||
*
|
||||
* Generate a UUID v4 format GUID.
|
||||
*
|
||||
* @returns 36 字符的 UUID 字符串 (例如: "550e8400-e29b-41d4-a716-446655440000")
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const id = generateGUID();
|
||||
* console.log(id); // "550e8400-e29b-41d4-a716-446655440000"
|
||||
* ```
|
||||
*/
|
||||
export function generateGUID(): string {
|
||||
// 优先使用原生 crypto API(浏览器和 Node.js 19+)
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
// 降级方案:使用 crypto.getRandomValues 或 Math.random
|
||||
return generateGUIDFallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* 降级 GUID 生成实现
|
||||
*
|
||||
* Fallback GUID generation using crypto.getRandomValues or Math.random.
|
||||
*/
|
||||
function generateGUIDFallback(): string {
|
||||
// 尝试使用 crypto.getRandomValues
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
||||
const bytes = new Uint8Array(16);
|
||||
crypto.getRandomValues(bytes);
|
||||
|
||||
// 设置版本号 (version 4)
|
||||
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
||||
// 设置变体 (variant 1)
|
||||
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
||||
|
||||
return formatUUID(bytes);
|
||||
}
|
||||
|
||||
// 最终降级:使用 Math.random(不推荐,但可用)
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 16 字节数组为 UUID 字符串
|
||||
*
|
||||
* Format 16-byte array to UUID string.
|
||||
*/
|
||||
function formatUUID(bytes: Uint8Array): string {
|
||||
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证字符串是否为有效的 UUID 格式
|
||||
*
|
||||
* Validate if a string is a valid UUID format.
|
||||
*
|
||||
* @param value - 要验证的字符串
|
||||
* @returns 如果是有效的 UUID 格式返回 true
|
||||
*/
|
||||
export function isValidGUID(value: string): boolean {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 空 GUID 常量
|
||||
*
|
||||
* Empty GUID constant (all zeros).
|
||||
*/
|
||||
export const EMPTY_GUID = '00000000-0000-0000-0000-000000000000';
|
||||
52
packages/framework/core/src/Utils/GlobalManager.ts
Normal file
52
packages/framework/core/src/Utils/GlobalManager.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 全局管理器的基类。所有全局管理器都应该从此类继承。
|
||||
*/
|
||||
export class GlobalManager {
|
||||
private _enabled: boolean = false;
|
||||
|
||||
/**
|
||||
* 获取或设置管理器是否启用
|
||||
*/
|
||||
public get enabled() {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
public set enabled(value: boolean) {
|
||||
this.setEnabled(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置管理器是否启用
|
||||
* @param isEnabled 如果为true,则启用管理器;否则禁用管理器
|
||||
*/
|
||||
public setEnabled(isEnabled: boolean) {
|
||||
if (this._enabled != isEnabled) {
|
||||
this._enabled = isEnabled;
|
||||
if (this._enabled) {
|
||||
// 如果启用了管理器,则调用onEnabled方法
|
||||
this.onEnabled();
|
||||
} else {
|
||||
// 如果禁用了管理器,则调用onDisabled方法
|
||||
this.onDisabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在启用管理器时调用的回调方法
|
||||
*/
|
||||
protected onEnabled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在禁用管理器时调用的回调方法
|
||||
*/
|
||||
protected onDisabled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新管理器状态的方法
|
||||
*/
|
||||
public update() {
|
||||
}
|
||||
}
|
||||
202
packages/framework/core/src/Utils/Logger/ConsoleLogger.ts
Normal file
202
packages/framework/core/src/Utils/Logger/ConsoleLogger.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { Colors, LogLevel } from './Constants';
|
||||
import { ILogger, LoggerColorConfig, LoggerConfig } from './Types';
|
||||
|
||||
|
||||
/**
|
||||
* 默认控制台日志实现
|
||||
*/
|
||||
export class ConsoleLogger implements ILogger {
|
||||
private _config: LoggerConfig;
|
||||
|
||||
constructor(config: Partial<LoggerConfig> = {}) {
|
||||
this._config = {
|
||||
level: LogLevel.Info,
|
||||
enableTimestamp: true,
|
||||
enableColors: typeof window === 'undefined',
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出调试级别日志
|
||||
* @param message 日志消息
|
||||
* @param args 附加参数
|
||||
*/
|
||||
public debug(message: string, ...args: unknown[]): void {
|
||||
this.log(LogLevel.Debug, message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出信息级别日志
|
||||
* @param message 日志消息
|
||||
* @param args 附加参数
|
||||
*/
|
||||
public info(message: string, ...args: unknown[]): void {
|
||||
this.log(LogLevel.Info, message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出警告级别日志
|
||||
* @param message 日志消息
|
||||
* @param args 附加参数
|
||||
*/
|
||||
public warn(message: string, ...args: unknown[]): void {
|
||||
this.log(LogLevel.Warn, message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出错误级别日志
|
||||
* @param message 日志消息
|
||||
* @param args 附加参数
|
||||
*/
|
||||
public error(message: string, ...args: unknown[]): void {
|
||||
this.log(LogLevel.Error, message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出致命错误级别日志
|
||||
* @param message 日志消息
|
||||
* @param args 附加参数
|
||||
*/
|
||||
public fatal(message: string, ...args: unknown[]): void {
|
||||
this.log(LogLevel.Fatal, message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志级别
|
||||
* @param level 日志级别
|
||||
*/
|
||||
public setLevel(level: LogLevel): void {
|
||||
this._config.level = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置颜色配置
|
||||
* @param colors 颜色配置
|
||||
*/
|
||||
public setColors(colors: LoggerColorConfig): void {
|
||||
if (Object.keys(colors).length === 0) {
|
||||
// 重置为默认颜色
|
||||
delete this._config.colors;
|
||||
} else {
|
||||
this._config.colors = {
|
||||
...this._config.colors,
|
||||
...colors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志前缀
|
||||
* @param prefix 前缀字符串
|
||||
*/
|
||||
public setPrefix(prefix: string): void {
|
||||
this._config.prefix = prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部日志输出方法
|
||||
* @param level 日志级别
|
||||
* @param message 日志消息
|
||||
* @param args 附加参数
|
||||
*/
|
||||
private log(level: LogLevel, message: string, ...args: unknown[]): void {
|
||||
if (level < this._config.level) {
|
||||
return;
|
||||
}
|
||||
|
||||
let formattedMessage = message;
|
||||
|
||||
// 添加时间戳
|
||||
if (this._config.enableTimestamp) {
|
||||
const timestamp = new Date().toISOString();
|
||||
formattedMessage = `[${timestamp}] ${formattedMessage}`;
|
||||
}
|
||||
|
||||
// 添加前缀
|
||||
if (this._config.prefix) {
|
||||
formattedMessage = `[${this._config.prefix}] ${formattedMessage}`;
|
||||
}
|
||||
|
||||
// 添加日志级别
|
||||
const levelName = LogLevel[level].toUpperCase();
|
||||
formattedMessage = `[${levelName}] ${formattedMessage}`;
|
||||
|
||||
// 使用自定义输出或默认控制台输出
|
||||
if (this._config.output) {
|
||||
this._config.output(level, formattedMessage);
|
||||
} else {
|
||||
this.outputToConsole(level, formattedMessage, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出到控制台
|
||||
* @param level 日志级别
|
||||
* @param message 格式化后的消息
|
||||
* @param args 附加参数
|
||||
*/
|
||||
private outputToConsole(level: LogLevel, message: string, ...args: unknown[]): void {
|
||||
const colors = this._config.enableColors ? this.getColors() : null;
|
||||
|
||||
switch (level) {
|
||||
case LogLevel.Debug:
|
||||
if (colors) {
|
||||
console.debug(`${colors.debug}${message}${colors.reset}`, ...args);
|
||||
} else {
|
||||
console.debug(message, ...args);
|
||||
}
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
if (colors) {
|
||||
console.info(`${colors.info}${message}${colors.reset}`, ...args);
|
||||
} else {
|
||||
console.info(message, ...args);
|
||||
}
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
if (colors) {
|
||||
console.warn(`${colors.warn}${message}${colors.reset}`, ...args);
|
||||
} else {
|
||||
console.warn(message, ...args);
|
||||
}
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
if (colors) {
|
||||
console.error(`${colors.error}${message}${colors.reset}`, ...args);
|
||||
} else {
|
||||
console.error(message, ...args);
|
||||
}
|
||||
break;
|
||||
case LogLevel.Fatal:
|
||||
if (colors) {
|
||||
console.error(`${colors.fatal}${message}${colors.reset}`, ...args);
|
||||
} else {
|
||||
console.error(message, ...args);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取控制台颜色配置
|
||||
* @returns 颜色配置对象
|
||||
*/
|
||||
private getColors() {
|
||||
// 默认颜色配置
|
||||
const defaultColors = {
|
||||
debug: Colors.BRIGHT_BLACK, // 灰色
|
||||
info: Colors.GREEN, // 绿色
|
||||
warn: Colors.YELLOW, // 黄色
|
||||
error: Colors.RED, // 红色
|
||||
fatal: Colors.BRIGHT_RED, // 亮红色
|
||||
reset: Colors.RESET // 重置
|
||||
};
|
||||
|
||||
// 合并用户自定义颜色
|
||||
return {
|
||||
...defaultColors,
|
||||
...this._config.colors
|
||||
};
|
||||
}
|
||||
}
|
||||
41
packages/framework/core/src/Utils/Logger/Constants.ts
Normal file
41
packages/framework/core/src/Utils/Logger/Constants.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 日志级别
|
||||
*/
|
||||
export enum LogLevel {
|
||||
Debug = 0,
|
||||
Info = 1,
|
||||
Warn = 2,
|
||||
Error = 3,
|
||||
Fatal = 4,
|
||||
None = 5
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的颜色常量
|
||||
*/
|
||||
export const Colors = {
|
||||
// 基础颜色
|
||||
BLACK: '\x1b[30m',
|
||||
RED: '\x1b[31m',
|
||||
GREEN: '\x1b[32m',
|
||||
YELLOW: '\x1b[33m',
|
||||
BLUE: '\x1b[34m',
|
||||
MAGENTA: '\x1b[35m',
|
||||
CYAN: '\x1b[36m',
|
||||
WHITE: '\x1b[37m',
|
||||
|
||||
// 亮色版本
|
||||
BRIGHT_BLACK: '\x1b[90m',
|
||||
BRIGHT_RED: '\x1b[91m',
|
||||
BRIGHT_GREEN: '\x1b[92m',
|
||||
BRIGHT_YELLOW: '\x1b[93m',
|
||||
BRIGHT_BLUE: '\x1b[94m',
|
||||
BRIGHT_MAGENTA: '\x1b[95m',
|
||||
BRIGHT_CYAN: '\x1b[96m',
|
||||
BRIGHT_WHITE: '\x1b[97m',
|
||||
|
||||
// 特殊
|
||||
RESET: '\x1b[0m',
|
||||
BOLD: '\x1b[1m',
|
||||
UNDERLINE: '\x1b[4m'
|
||||
} as const;
|
||||
196
packages/framework/core/src/Utils/Logger/LoggerManager.ts
Normal file
196
packages/framework/core/src/Utils/Logger/LoggerManager.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { ConsoleLogger } from './ConsoleLogger';
|
||||
import { LogLevel } from './Constants';
|
||||
import { ILogger, LoggerColorConfig } from './Types';
|
||||
|
||||
/**
|
||||
* 日志管理器
|
||||
*/
|
||||
export class LoggerManager {
|
||||
private static _instance: LoggerManager;
|
||||
private _loggers = new Map<string, ILogger>();
|
||||
private _defaultLogger?: ILogger;
|
||||
private _defaultLevel = LogLevel.Info;
|
||||
private _loggerFactory?: (name?: string) => ILogger;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
private get defaultLogger(): ILogger {
|
||||
if (!this._defaultLogger) {
|
||||
this._defaultLogger = this.createDefaultLogger();
|
||||
}
|
||||
return this._defaultLogger;
|
||||
}
|
||||
|
||||
// 新增: 创建默认 logger 的逻辑
|
||||
private createDefaultLogger(): ILogger {
|
||||
if (this._loggerFactory) {
|
||||
return this._loggerFactory();
|
||||
}
|
||||
return new ConsoleLogger({ level: this._defaultLevel });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志管理器实例
|
||||
* @returns 日志管理器实例
|
||||
*/
|
||||
public static getInstance(): LoggerManager {
|
||||
if (!LoggerManager._instance) {
|
||||
LoggerManager._instance = new LoggerManager();
|
||||
}
|
||||
return LoggerManager._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建日志器
|
||||
* @param name 日志器名称
|
||||
* @returns 日志器实例
|
||||
*/
|
||||
public getLogger(name?: string): ILogger {
|
||||
if (!name) {
|
||||
return this.defaultLogger;
|
||||
}
|
||||
|
||||
// 如果有自定义 factory, 每次都调用(不缓存), 由使用方自行管理
|
||||
if (this._loggerFactory) {
|
||||
return this._loggerFactory(name);
|
||||
}
|
||||
|
||||
// 默认 ConsoleLogger 仍然缓存(保持向后兼容)
|
||||
if (!this._loggers.has(name)) {
|
||||
this._loggers.set(name, new ConsoleLogger({ prefix: name, level: this._defaultLevel }));
|
||||
}
|
||||
|
||||
return this._loggers.get(name)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志器
|
||||
* @param name 日志器名称
|
||||
* @param logger 日志器实例
|
||||
*/
|
||||
public setLogger(name: string, logger: ILogger): void {
|
||||
this._loggers.set(name, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局日志级别
|
||||
* @param level 日志级别
|
||||
*/
|
||||
public setGlobalLevel(level: LogLevel): void {
|
||||
this._defaultLevel = level;
|
||||
|
||||
if (this._defaultLogger instanceof ConsoleLogger) {
|
||||
this._defaultLogger.setLevel(level);
|
||||
}
|
||||
|
||||
for (const logger of this._loggers.values()) {
|
||||
if (logger instanceof ConsoleLogger) {
|
||||
logger.setLevel(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建子日志器
|
||||
* @param parentName 父日志器名称
|
||||
* @param childName 子日志器名称
|
||||
* @returns 子日志器实例
|
||||
*/
|
||||
public createChildLogger(parentName: string, childName: string): ILogger {
|
||||
const fullName = `${parentName}.${childName}`;
|
||||
return this.getLogger(fullName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局颜色配置
|
||||
* @param colors 颜色配置
|
||||
*/
|
||||
public setGlobalColors(colors: LoggerColorConfig): void {
|
||||
if (this._defaultLogger instanceof ConsoleLogger) {
|
||||
this._defaultLogger.setColors(colors);
|
||||
}
|
||||
|
||||
for (const logger of this._loggers.values()) {
|
||||
if (logger instanceof ConsoleLogger) {
|
||||
logger.setColors(colors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置为默认颜色配置
|
||||
*/
|
||||
public resetColors(): void {
|
||||
if (this._defaultLogger instanceof ConsoleLogger) {
|
||||
this._defaultLogger.setColors({});
|
||||
}
|
||||
|
||||
for (const logger of this._loggers.values()) {
|
||||
if (logger instanceof ConsoleLogger) {
|
||||
logger.setColors({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志器工厂方法
|
||||
* @param factory 日志器工厂方法
|
||||
*
|
||||
* 注意: 应该在导入 ECS 模块之前调用此方法。
|
||||
* 设置后, 每次调用 getLogger() 都会通过 factory 创建新的 logger 实例, 由用户侧管理
|
||||
*/
|
||||
public setLoggerFactory(factory: (name?: string) => ILogger): void {
|
||||
this._loggerFactory = factory;
|
||||
|
||||
// 清空默认 logger,下次获取时使用新工厂方法
|
||||
delete this._defaultLogger;
|
||||
|
||||
// 清空缓存
|
||||
this._loggers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认日志器实例
|
||||
*/
|
||||
export const Logger = LoggerManager.getInstance().getLogger();
|
||||
|
||||
/**
|
||||
* 创建命名日志器
|
||||
* @param name 日志器名称
|
||||
* @returns 日志器实例
|
||||
*/
|
||||
export function createLogger(name: string): ILogger {
|
||||
return LoggerManager.getInstance().getLogger(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局日志颜色配置
|
||||
* @param colors 颜色配置
|
||||
*/
|
||||
export function setLoggerColors(colors: LoggerColorConfig): void {
|
||||
LoggerManager.getInstance().setGlobalColors(colors);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置日志颜色为默认配置
|
||||
*/
|
||||
export function resetLoggerColors(): void {
|
||||
LoggerManager.getInstance().resetColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局日志级别
|
||||
* @param level 日志级别
|
||||
*/
|
||||
export function setGlobalLogLevel(level: LogLevel): void {
|
||||
LoggerManager.getInstance().setGlobalLevel(level);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志器工厂方法
|
||||
* @param factory 日志器工厂方法
|
||||
*/
|
||||
export function setLoggerFactory(factory: (name?: string) => ILogger): void {
|
||||
LoggerManager.getInstance().setLoggerFactory(factory);
|
||||
}
|
||||
42
packages/framework/core/src/Utils/Logger/Types.ts
Normal file
42
packages/framework/core/src/Utils/Logger/Types.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { LogLevel } from './Constants';
|
||||
|
||||
/**
|
||||
* 日志接口
|
||||
*/
|
||||
export type ILogger = {
|
||||
debug(...args: unknown[]): void;
|
||||
info(...args: unknown[]): void;
|
||||
warn(...args: unknown[]): void;
|
||||
error(...args: unknown[]): void;
|
||||
fatal(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志颜色配置接口
|
||||
*/
|
||||
export type LoggerColorConfig = {
|
||||
debug?: string;
|
||||
info?: string;
|
||||
warn?: string;
|
||||
error?: string;
|
||||
fatal?: string;
|
||||
reset?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志配置
|
||||
*/
|
||||
export type LoggerConfig = {
|
||||
/** 日志级别 */
|
||||
level: LogLevel;
|
||||
/** 是否启用时间戳 */
|
||||
enableTimestamp: boolean;
|
||||
/** 是否启用颜色 */
|
||||
enableColors: boolean;
|
||||
/** 日志前缀 */
|
||||
prefix?: string;
|
||||
/** 自定义输出函数 */
|
||||
output?: (level: LogLevel, message: string) => void;
|
||||
/** 自定义颜色配置 */
|
||||
colors?: LoggerColorConfig;
|
||||
}
|
||||
4
packages/framework/core/src/Utils/Logger/index.ts
Normal file
4
packages/framework/core/src/Utils/Logger/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './ConsoleLogger';
|
||||
export * from './Constants';
|
||||
export * from './LoggerManager';
|
||||
export * from './Types';
|
||||
383
packages/framework/core/src/Utils/PerformanceMonitor.ts
Normal file
383
packages/framework/core/src/Utils/PerformanceMonitor.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* 性能监控数据
|
||||
*/
|
||||
export type PerformanceData = {
|
||||
/** 系统名称 */
|
||||
name: string;
|
||||
/** 执行时间(毫秒) */
|
||||
executionTime: number;
|
||||
/** 处理的实体数量 */
|
||||
entityCount: number;
|
||||
/** 平均每个实体的处理时间 */
|
||||
averageTimePerEntity: number;
|
||||
/** 最后更新时间戳 */
|
||||
lastUpdateTime: number;
|
||||
/** 内存使用量(字节) */
|
||||
memoryUsage?: number;
|
||||
/** CPU使用率(百分比) */
|
||||
cpuUsage?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能统计信息
|
||||
*/
|
||||
export type PerformanceStats = {
|
||||
/** 总执行时间 */
|
||||
totalTime: number;
|
||||
/** 平均执行时间 */
|
||||
averageTime: number;
|
||||
/** 最小执行时间 */
|
||||
minTime: number;
|
||||
/** 最大执行时间 */
|
||||
maxTime: number;
|
||||
/** 执行次数 */
|
||||
executionCount: number;
|
||||
/** 最近的执行时间列表 */
|
||||
recentTimes: number[];
|
||||
/** 标准差 */
|
||||
standardDeviation: number;
|
||||
/** 95百分位数 */
|
||||
percentile95: number;
|
||||
/** 99百分位数 */
|
||||
percentile99: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能警告类型
|
||||
*/
|
||||
export enum PerformanceWarningType {
|
||||
HIGH_EXECUTION_TIME = 'high_execution_time',
|
||||
HIGH_MEMORY_USAGE = 'high_memory_usage',
|
||||
HIGH_CPU_USAGE = 'high_cpu_usage',
|
||||
FREQUENT_GC = 'frequent_gc',
|
||||
LOW_FPS = 'low_fps',
|
||||
HIGH_ENTITY_COUNT = 'high_entity_count'
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能警告
|
||||
*/
|
||||
export type PerformanceWarning = {
|
||||
type: PerformanceWarningType;
|
||||
systemName: string;
|
||||
message: string;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
timestamp: number;
|
||||
value: number;
|
||||
threshold: number;
|
||||
suggestion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能阈值配置
|
||||
*/
|
||||
export type PerformanceThresholds = {
|
||||
/** 执行时间阈值(毫秒) */
|
||||
executionTime: {
|
||||
warning: number;
|
||||
critical: number;
|
||||
};
|
||||
/** 内存使用阈值(MB) */
|
||||
memoryUsage: {
|
||||
warning: number;
|
||||
critical: number;
|
||||
};
|
||||
/** CPU使用率阈值(百分比) */
|
||||
cpuUsage: {
|
||||
warning: number;
|
||||
critical: number;
|
||||
};
|
||||
/** FPS阈值 */
|
||||
fps: {
|
||||
warning: number;
|
||||
critical: number;
|
||||
};
|
||||
/** 实体数量阈值 */
|
||||
entityCount: {
|
||||
warning: number;
|
||||
critical: number;
|
||||
};
|
||||
}
|
||||
|
||||
import type { IService } from '../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 高性能监控器
|
||||
* 用于监控ECS系统的性能表现,提供详细的分析和优化建议
|
||||
*/
|
||||
export class PerformanceMonitor implements IService {
|
||||
private _systemData = new Map<string, PerformanceData>();
|
||||
private _systemStats = new Map<string, PerformanceStats>();
|
||||
private _isEnabled = false;
|
||||
private _maxRecentSamples = 60; // 保留最近60帧的数据
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* @zh 更新FPS统计(可选功能,子类可重写)
|
||||
* @en Update FPS statistics (optional, can be overridden by subclasses)
|
||||
*
|
||||
* @param _deltaTime - @zh 帧时间间隔(秒)@en Frame delta time in seconds
|
||||
*/
|
||||
public updateFPS(_deltaTime: number): void {
|
||||
// Base implementation does nothing - override in subclass for FPS tracking
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用性能监控
|
||||
*/
|
||||
public enable(): void {
|
||||
this._isEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用性能监控
|
||||
*/
|
||||
public disable(): void {
|
||||
this._isEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用了性能监控
|
||||
*/
|
||||
public get isEnabled(): boolean {
|
||||
return this._isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始监控系统性能
|
||||
* @param systemName 系统名称
|
||||
* @returns 开始时间戳
|
||||
*/
|
||||
public startMonitoring(_systemName: string): number {
|
||||
if (!this._isEnabled) {
|
||||
return 0;
|
||||
}
|
||||
return performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束监控并记录性能数据
|
||||
* @param systemName 系统名称
|
||||
* @param startTime 开始时间戳
|
||||
* @param entityCount 处理的实体数量
|
||||
*/
|
||||
public endMonitoring(systemName: string, startTime: number, entityCount: number = 0): void {
|
||||
if (!this._isEnabled || startTime === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const executionTime = endTime - startTime;
|
||||
const averageTimePerEntity = entityCount > 0 ? executionTime / entityCount : 0;
|
||||
|
||||
// 更新当前性能数据
|
||||
const data: PerformanceData = {
|
||||
name: systemName,
|
||||
executionTime,
|
||||
entityCount,
|
||||
averageTimePerEntity,
|
||||
lastUpdateTime: endTime
|
||||
};
|
||||
|
||||
this._systemData.set(systemName, data);
|
||||
|
||||
// 更新统计信息
|
||||
this.updateStats(systemName, executionTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新系统统计信息
|
||||
* @param systemName 系统名称
|
||||
* @param executionTime 执行时间
|
||||
*/
|
||||
private updateStats(systemName: string, executionTime: number): void {
|
||||
let stats = this._systemStats.get(systemName);
|
||||
|
||||
if (!stats) {
|
||||
stats = {
|
||||
totalTime: 0,
|
||||
averageTime: 0,
|
||||
minTime: Number.MAX_VALUE,
|
||||
maxTime: 0,
|
||||
executionCount: 0,
|
||||
recentTimes: [],
|
||||
standardDeviation: 0,
|
||||
percentile95: 0,
|
||||
percentile99: 0
|
||||
};
|
||||
this._systemStats.set(systemName, stats);
|
||||
}
|
||||
|
||||
// 更新基本统计
|
||||
stats.totalTime += executionTime;
|
||||
stats.executionCount++;
|
||||
stats.averageTime = stats.totalTime / stats.executionCount;
|
||||
stats.minTime = Math.min(stats.minTime, executionTime);
|
||||
stats.maxTime = Math.max(stats.maxTime, executionTime);
|
||||
|
||||
// 更新最近时间列表
|
||||
stats.recentTimes.push(executionTime);
|
||||
if (stats.recentTimes.length > this._maxRecentSamples) {
|
||||
stats.recentTimes.shift();
|
||||
}
|
||||
|
||||
// 计算高级统计信息
|
||||
this.calculateAdvancedStats(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算高级统计信息
|
||||
* @param stats 统计信息对象
|
||||
*/
|
||||
private calculateAdvancedStats(stats: PerformanceStats): void {
|
||||
if (stats.recentTimes.length === 0) return;
|
||||
|
||||
// 计算标准差
|
||||
const mean = stats.recentTimes.reduce((a, b) => a + b, 0) / stats.recentTimes.length;
|
||||
const variance = stats.recentTimes.reduce((acc, time) => acc + Math.pow(time - mean, 2), 0) / stats.recentTimes.length;
|
||||
stats.standardDeviation = Math.sqrt(variance);
|
||||
|
||||
// 计算百分位数
|
||||
const sortedTimes = [...stats.recentTimes].sort((a, b) => a - b);
|
||||
const len = sortedTimes.length;
|
||||
|
||||
stats.percentile95 = sortedTimes[Math.floor(len * 0.95)] || 0;
|
||||
stats.percentile99 = sortedTimes[Math.floor(len * 0.99)] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统的当前性能数据
|
||||
* @param systemName 系统名称
|
||||
* @returns 性能数据或undefined
|
||||
*/
|
||||
public getSystemData(systemName: string): PerformanceData | undefined {
|
||||
return this._systemData.get(systemName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统的统计信息
|
||||
* @param systemName 系统名称
|
||||
* @returns 统计信息或undefined
|
||||
*/
|
||||
public getSystemStats(systemName: string): PerformanceStats | undefined {
|
||||
return this._systemStats.get(systemName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有系统的性能数据
|
||||
* @returns 所有系统的性能数据
|
||||
*/
|
||||
public getAllSystemData(): Map<string, PerformanceData> {
|
||||
return new Map(this._systemData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有系统的统计信息
|
||||
* @returns 所有系统的统计信息
|
||||
*/
|
||||
public getAllSystemStats(): Map<string, PerformanceStats> {
|
||||
return new Map(this._systemStats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能报告
|
||||
* @returns 格式化的性能报告字符串
|
||||
*/
|
||||
public getPerformanceReport(): string {
|
||||
if (!this._isEnabled) {
|
||||
return 'Performance monitoring is disabled.';
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('=== ECS Performance Report ===');
|
||||
lines.push('');
|
||||
|
||||
// 按平均执行时间排序
|
||||
const sortedSystems = Array.from(this._systemStats.entries())
|
||||
.sort((a, b) => b[1].averageTime - a[1].averageTime);
|
||||
|
||||
for (const [systemName, stats] of sortedSystems) {
|
||||
const data = this._systemData.get(systemName);
|
||||
|
||||
lines.push(`System: ${systemName}`);
|
||||
lines.push(` Current: ${data?.executionTime.toFixed(2)}ms (${data?.entityCount} entities)`);
|
||||
lines.push(` Average: ${stats.averageTime.toFixed(2)}ms`);
|
||||
lines.push(` Min/Max: ${stats.minTime.toFixed(2)}ms / ${stats.maxTime.toFixed(2)}ms`);
|
||||
lines.push(` Total: ${stats.totalTime.toFixed(2)}ms (${stats.executionCount} calls)`);
|
||||
|
||||
if (data?.averageTimePerEntity && data.averageTimePerEntity > 0) {
|
||||
lines.push(` Per Entity: ${data.averageTimePerEntity.toFixed(4)}ms`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// 总体统计
|
||||
const totalCurrentTime = Array.from(this._systemData.values())
|
||||
.reduce((sum, data) => sum + data.executionTime, 0);
|
||||
|
||||
lines.push(`Total Frame Time: ${totalCurrentTime.toFixed(2)}ms`);
|
||||
lines.push(`Systems Count: ${this._systemData.size}`);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有性能数据
|
||||
*/
|
||||
public reset(): void {
|
||||
this._systemData.clear();
|
||||
this._systemStats.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定系统的性能数据
|
||||
* @param systemName 系统名称
|
||||
*/
|
||||
public resetSystem(systemName: string): void {
|
||||
this._systemData.delete(systemName);
|
||||
this._systemStats.delete(systemName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能警告
|
||||
* @param thresholdMs 警告阈值(毫秒)
|
||||
* @returns 超过阈值的系统列表
|
||||
*/
|
||||
public getPerformanceWarnings(thresholdMs: number = 16.67): string[] {
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (const [systemName, data] of this._systemData.entries()) {
|
||||
if (data.executionTime > thresholdMs) {
|
||||
warnings.push(`${systemName}: ${data.executionTime.toFixed(2)}ms (>${thresholdMs}ms)`);
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大保留样本数
|
||||
* @param maxSamples 最大样本数
|
||||
*/
|
||||
public setMaxRecentSamples(maxSamples: number): void {
|
||||
this._maxRecentSamples = maxSamples;
|
||||
|
||||
// 裁剪现有数据
|
||||
for (const stats of this._systemStats.values()) {
|
||||
while (stats.recentTimes.length > maxSamples) {
|
||||
stats.recentTimes.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._systemData.clear();
|
||||
this._systemStats.clear();
|
||||
this._isEnabled = false;
|
||||
}
|
||||
}
|
||||
29
packages/framework/core/src/Utils/Pool/IPoolable.ts
Normal file
29
packages/framework/core/src/Utils/Pool/IPoolable.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 可池化对象接口
|
||||
*/
|
||||
export type IPoolable = {
|
||||
/**
|
||||
* 重置对象状态,准备重用
|
||||
*/
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象池统计信息
|
||||
*/
|
||||
export type PoolStats = {
|
||||
/** 池中对象数量 */
|
||||
size: number;
|
||||
/** 池的最大大小 */
|
||||
maxSize: number;
|
||||
/** 总共创建的对象数量 */
|
||||
totalCreated: number;
|
||||
/** 总共获取的次数 */
|
||||
totalObtained: number;
|
||||
/** 总共释放的次数 */
|
||||
totalReleased: number;
|
||||
/** 命中率(从池中获取的比例) */
|
||||
hitRate: number;
|
||||
/** 内存使用估算(字节) */
|
||||
estimatedMemoryUsage: number;
|
||||
}
|
||||
282
packages/framework/core/src/Utils/Pool/Pool.ts
Normal file
282
packages/framework/core/src/Utils/Pool/Pool.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import { IPoolable, PoolStats } from './IPoolable';
|
||||
|
||||
/**
|
||||
* 高性能通用对象池
|
||||
* 支持任意类型的对象池化,包含详细的统计信息
|
||||
*/
|
||||
export class Pool<T extends IPoolable> {
|
||||
private static _pools = new Map<Function, Pool<any>>();
|
||||
|
||||
private _objects: T[] = [];
|
||||
private _createFn: () => T;
|
||||
private _maxSize: number;
|
||||
private _stats: PoolStats;
|
||||
private _objectSize: number; // 估算的单个对象大小
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param createFn 创建对象的函数
|
||||
* @param maxSize 池的最大大小,默认100
|
||||
* @param estimatedObjectSize 估算的单个对象大小(字节),默认1024
|
||||
*/
|
||||
constructor(createFn: () => T, maxSize: number = 100, estimatedObjectSize: number = 1024) {
|
||||
this._createFn = createFn;
|
||||
this._maxSize = maxSize;
|
||||
this._objectSize = estimatedObjectSize;
|
||||
this._stats = {
|
||||
size: 0,
|
||||
maxSize,
|
||||
totalCreated: 0,
|
||||
totalObtained: 0,
|
||||
totalReleased: 0,
|
||||
hitRate: 0,
|
||||
estimatedMemoryUsage: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型的对象池
|
||||
* @param type 对象类型
|
||||
* @param maxSize 池的最大大小
|
||||
* @param estimatedObjectSize 估算的单个对象大小
|
||||
* @returns 对象池实例
|
||||
*/
|
||||
public static getPool<T extends IPoolable>(
|
||||
type: new (...args: unknown[]) => T,
|
||||
maxSize: number = 100,
|
||||
estimatedObjectSize: number = 1024
|
||||
): Pool<T> {
|
||||
let pool = this._pools.get(type);
|
||||
|
||||
if (!pool) {
|
||||
pool = new Pool<T>(() => new type(), maxSize, estimatedObjectSize);
|
||||
this._pools.set(type, pool);
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从池中获取对象
|
||||
* @returns 对象实例
|
||||
*/
|
||||
public obtain(): T {
|
||||
this._stats.totalObtained++;
|
||||
|
||||
if (this._objects.length > 0) {
|
||||
const obj = this._objects.pop()!;
|
||||
this._stats.size--;
|
||||
this._updateHitRate();
|
||||
this._updateMemoryUsage();
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 池中没有可用对象,创建新对象
|
||||
this._stats.totalCreated++;
|
||||
this._updateHitRate();
|
||||
return this._createFn();
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放对象回池中
|
||||
* @param obj 要释放的对象
|
||||
*/
|
||||
public release(obj: T): void {
|
||||
if (!obj) return;
|
||||
|
||||
this._stats.totalReleased++;
|
||||
|
||||
// 如果池未满,将对象放回池中
|
||||
if (this._stats.size < this._maxSize) {
|
||||
// 重置对象状态
|
||||
obj.reset();
|
||||
this._objects.push(obj);
|
||||
this._stats.size++;
|
||||
this._updateMemoryUsage();
|
||||
}
|
||||
// 如果池已满,让对象被垃圾回收
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取池统计信息
|
||||
* @returns 统计信息对象
|
||||
*/
|
||||
public getStats(): Readonly<PoolStats> {
|
||||
return { ...this._stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空池
|
||||
*/
|
||||
public clear(): void {
|
||||
// 重置所有对象
|
||||
for (const obj of this._objects) {
|
||||
obj.reset();
|
||||
}
|
||||
|
||||
this._objects.length = 0;
|
||||
this._stats.size = 0;
|
||||
this._updateMemoryUsage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩池(移除多余的对象)
|
||||
* @param targetSize 目标大小,默认为当前大小的一半
|
||||
*/
|
||||
public compact(targetSize?: number): void {
|
||||
const target = targetSize ?? Math.floor(this._objects.length / 2);
|
||||
|
||||
while (this._objects.length > target) {
|
||||
const obj = this._objects.pop();
|
||||
if (obj) {
|
||||
obj.reset();
|
||||
this._stats.size--;
|
||||
}
|
||||
}
|
||||
|
||||
this._updateMemoryUsage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 预填充池
|
||||
* @param count 预填充的对象数量
|
||||
*/
|
||||
public prewarm(count: number): void {
|
||||
const actualCount = Math.min(count, this._maxSize - this._objects.length);
|
||||
|
||||
for (let i = 0; i < actualCount; i++) {
|
||||
const obj = this._createFn();
|
||||
obj.reset();
|
||||
this._objects.push(obj);
|
||||
this._stats.totalCreated++;
|
||||
this._stats.size++;
|
||||
}
|
||||
|
||||
this._updateMemoryUsage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大池大小
|
||||
* @param maxSize 新的最大大小
|
||||
*/
|
||||
public setMaxSize(maxSize: number): void {
|
||||
this._maxSize = maxSize;
|
||||
this._stats.maxSize = maxSize;
|
||||
|
||||
// 如果当前池大小超过新的最大值,进行压缩
|
||||
if (this._objects.length > maxSize) {
|
||||
this.compact(maxSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取池中可用对象数量
|
||||
* @returns 可用对象数量
|
||||
*/
|
||||
public getAvailableCount(): number {
|
||||
return this._objects.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查池是否为空
|
||||
* @returns 如果池为空返回true
|
||||
*/
|
||||
public isEmpty(): boolean {
|
||||
return this._objects.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查池是否已满
|
||||
* @returns 如果池已满返回true
|
||||
*/
|
||||
public isFull(): boolean {
|
||||
return this._objects.length >= this._maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的池类型
|
||||
* @returns 所有池类型的数组
|
||||
*/
|
||||
public static getAllPoolTypes(): Function[] {
|
||||
return Array.from(this._pools.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有池的统计信息
|
||||
* @returns 包含所有池统计信息的对象
|
||||
*/
|
||||
public static getAllPoolStats(): Record<string, PoolStats> {
|
||||
const stats: Record<string, PoolStats> = {};
|
||||
|
||||
for (const [type, pool] of this._pools) {
|
||||
const typeName = type.name || type.toString();
|
||||
stats[typeName] = pool.getStats();
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩所有池
|
||||
*/
|
||||
public static compactAllPools(): void {
|
||||
for (const pool of this._pools.values()) {
|
||||
pool.compact();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有池
|
||||
*/
|
||||
public static clearAllPools(): void {
|
||||
for (const pool of this._pools.values()) {
|
||||
pool.clear();
|
||||
}
|
||||
this._pools.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局池统计信息的格式化字符串
|
||||
* @returns 格式化的统计信息字符串
|
||||
*/
|
||||
public static getGlobalStatsString(): string {
|
||||
const stats = this.getAllPoolStats();
|
||||
const lines: string[] = ['=== Object Pool Global Statistics ===', ''];
|
||||
|
||||
if (Object.keys(stats).length === 0) {
|
||||
lines.push('No pools registered');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
for (const [typeName, stat] of Object.entries(stats)) {
|
||||
lines.push(`${typeName}:`);
|
||||
lines.push(` Size: ${stat.size}/${stat.maxSize}`);
|
||||
lines.push(` Hit Rate: ${(stat.hitRate * 100).toFixed(1)}%`);
|
||||
lines.push(` Total Created: ${stat.totalCreated}`);
|
||||
lines.push(` Total Obtained: ${stat.totalObtained}`);
|
||||
lines.push(` Memory: ${(stat.estimatedMemoryUsage / 1024).toFixed(1)} KB`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新命中率
|
||||
*/
|
||||
private _updateHitRate(): void {
|
||||
if (this._stats.totalObtained === 0) {
|
||||
this._stats.hitRate = 0;
|
||||
} else {
|
||||
const hits = this._stats.totalObtained - this._stats.totalCreated;
|
||||
this._stats.hitRate = hits / this._stats.totalObtained;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新内存使用估算
|
||||
*/
|
||||
private _updateMemoryUsage(): void {
|
||||
this._stats.estimatedMemoryUsage = this._stats.size * this._objectSize;
|
||||
}
|
||||
}
|
||||
236
packages/framework/core/src/Utils/Pool/PoolManager.ts
Normal file
236
packages/framework/core/src/Utils/Pool/PoolManager.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { IPoolable, PoolStats } from './IPoolable';
|
||||
import { Pool } from './Pool';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
|
||||
/**
|
||||
* 池管理器
|
||||
* 统一管理所有对象池
|
||||
*/
|
||||
export class PoolManager implements IService {
|
||||
private pools = new Map<string, Pool<any>>();
|
||||
private autoCompactInterval = 60000; // 60秒
|
||||
private lastCompactTime = 0;
|
||||
|
||||
constructor() {
|
||||
// 普通构造函数,不再使用单例模式
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册池
|
||||
* @param name 池名称
|
||||
* @param pool 池实例
|
||||
*/
|
||||
public registerPool<T extends IPoolable>(name: string, pool: Pool<T>): void {
|
||||
this.pools.set(name, pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取池
|
||||
* @param name 池名称
|
||||
* @returns 池实例
|
||||
*/
|
||||
public getPool<T extends IPoolable>(name: string): Pool<T> | null {
|
||||
return this.pools.get(name) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新池管理器(应在游戏循环中调用)
|
||||
*/
|
||||
public update(): void {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - this.lastCompactTime > this.autoCompactInterval) {
|
||||
this.compactAllPools();
|
||||
this.lastCompactTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或获取标准池
|
||||
* @param name 池名称
|
||||
* @param createFn 创建函数
|
||||
* @param maxSize 最大大小
|
||||
* @param estimatedObjectSize 估算对象大小
|
||||
* @returns 池实例
|
||||
*/
|
||||
public createPool<T extends IPoolable>(
|
||||
name: string,
|
||||
createFn: () => T,
|
||||
maxSize: number = 100,
|
||||
estimatedObjectSize: number = 1024
|
||||
): Pool<T> {
|
||||
let pool = this.pools.get(name) as Pool<T>;
|
||||
|
||||
if (!pool) {
|
||||
pool = new Pool(createFn, maxSize, estimatedObjectSize);
|
||||
this.pools.set(name, pool);
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移除池
|
||||
* @param name 池名称
|
||||
* @returns 是否成功移除
|
||||
*/
|
||||
public removePool(name: string): boolean {
|
||||
const pool = this.pools.get(name);
|
||||
if (pool) {
|
||||
pool.clear();
|
||||
this.pools.delete(name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有池名称
|
||||
* @returns 池名称数组
|
||||
*/
|
||||
public getPoolNames(): string[] {
|
||||
return Array.from(this.pools.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取池数量
|
||||
* @returns 池数量
|
||||
*/
|
||||
public getPoolCount(): number {
|
||||
return this.pools.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩所有池
|
||||
*/
|
||||
public compactAllPools(): void {
|
||||
for (const pool of this.pools.values()) {
|
||||
pool.compact();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有池
|
||||
*/
|
||||
public clearAllPools(): void {
|
||||
for (const pool of this.pools.values()) {
|
||||
pool.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有池的统计信息
|
||||
* @returns 统计信息映射
|
||||
*/
|
||||
public getAllStats(): Map<string, PoolStats> {
|
||||
const stats = new Map<string, PoolStats>();
|
||||
|
||||
for (const [name, pool] of this.pools) {
|
||||
stats.set(name, pool.getStats());
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取总体统计信息
|
||||
* @returns 总体统计信息
|
||||
*/
|
||||
public getGlobalStats(): PoolStats {
|
||||
let totalSize = 0;
|
||||
let totalMaxSize = 0;
|
||||
let totalCreated = 0;
|
||||
let totalObtained = 0;
|
||||
let totalReleased = 0;
|
||||
let totalMemoryUsage = 0;
|
||||
|
||||
for (const pool of this.pools.values()) {
|
||||
const stats = pool.getStats();
|
||||
totalSize += stats.size;
|
||||
totalMaxSize += stats.maxSize;
|
||||
totalCreated += stats.totalCreated;
|
||||
totalObtained += stats.totalObtained;
|
||||
totalReleased += stats.totalReleased;
|
||||
totalMemoryUsage += stats.estimatedMemoryUsage;
|
||||
}
|
||||
|
||||
const hitRate = totalObtained === 0 ? 0 : (totalObtained - totalCreated) / totalObtained;
|
||||
|
||||
return {
|
||||
size: totalSize,
|
||||
maxSize: totalMaxSize,
|
||||
totalCreated,
|
||||
totalObtained,
|
||||
totalReleased,
|
||||
hitRate,
|
||||
estimatedMemoryUsage: totalMemoryUsage
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格式化的统计信息字符串
|
||||
* @returns 格式化字符串
|
||||
*/
|
||||
public getStatsString(): string {
|
||||
const lines: string[] = ['=== Pool Manager Statistics ===', ''];
|
||||
|
||||
if (this.pools.size === 0) {
|
||||
lines.push('No pools registered');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const globalStats = this.getGlobalStats();
|
||||
lines.push(`Total Pools: ${this.pools.size}`);
|
||||
lines.push(`Global Hit Rate: ${(globalStats.hitRate * 100).toFixed(1)}%`);
|
||||
lines.push(`Global Memory Usage: ${(globalStats.estimatedMemoryUsage / 1024).toFixed(1)} KB`);
|
||||
lines.push('');
|
||||
|
||||
for (const [name, pool] of this.pools) {
|
||||
const stats = pool.getStats();
|
||||
lines.push(`${name}:`);
|
||||
lines.push(` Size: ${stats.size}/${stats.maxSize}`);
|
||||
lines.push(` Hit Rate: ${(stats.hitRate * 100).toFixed(1)}%`);
|
||||
lines.push(` Memory: ${(stats.estimatedMemoryUsage / 1024).toFixed(1)} KB`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自动压缩间隔
|
||||
* @param intervalMs 间隔毫秒数
|
||||
*/
|
||||
public setAutoCompactInterval(intervalMs: number): void {
|
||||
this.autoCompactInterval = intervalMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预填充所有池
|
||||
*/
|
||||
public prewarmAllPools(): void {
|
||||
for (const pool of this.pools.values()) {
|
||||
const stats = pool.getStats();
|
||||
const prewarmCount = Math.floor(stats.maxSize * 0.2); // 预填充20%
|
||||
pool.prewarm(prewarmCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置池管理器
|
||||
*/
|
||||
public reset(): void {
|
||||
this.clearAllPools();
|
||||
this.pools.clear();
|
||||
this.lastCompactTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
* 实现 IService 接口
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
3
packages/framework/core/src/Utils/Pool/index.ts
Normal file
3
packages/framework/core/src/Utils/Pool/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './IPoolable';
|
||||
export * from './Pool';
|
||||
export * from './PoolManager';
|
||||
649
packages/framework/core/src/Utils/Profiler/AutoProfiler.ts
Normal file
649
packages/framework/core/src/Utils/Profiler/AutoProfiler.ts
Normal file
@@ -0,0 +1,649 @@
|
||||
/**
|
||||
* 自动性能分析器
|
||||
*
|
||||
* 提供自动函数包装和采样分析功能,无需手动埋点即可收集性能数据。
|
||||
*
|
||||
* 支持三种分析模式:
|
||||
* 1. 自动包装模式 - 使用 Proxy 自动包装类的所有方法
|
||||
* 2. 采样分析模式 - 定时采样调用栈(需要浏览器支持)
|
||||
* 3. 装饰器模式 - 使用 @Profile() 装饰器手动标记方法
|
||||
*/
|
||||
|
||||
import { ProfilerSDK } from './ProfilerSDK';
|
||||
import { ProfileCategory } from './ProfilerTypes';
|
||||
|
||||
/**
|
||||
* 自动分析配置
|
||||
*/
|
||||
export type AutoProfilerConfig = {
|
||||
/** 是否启用自动包装 */
|
||||
enabled: boolean;
|
||||
/** 采样间隔(毫秒),用于采样分析器 */
|
||||
sampleInterval: number;
|
||||
/** 最小记录耗时(毫秒),低于此值的调用不记录 */
|
||||
minDuration: number;
|
||||
/** 是否追踪异步方法 */
|
||||
trackAsync: boolean;
|
||||
/** 排除的方法名模式 */
|
||||
excludePatterns: RegExp[];
|
||||
/** 最大采样缓冲区大小 */
|
||||
maxBufferSize: number;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: AutoProfilerConfig = {
|
||||
enabled: true,
|
||||
sampleInterval: 10,
|
||||
minDuration: 0.1, // 0.1ms
|
||||
trackAsync: true,
|
||||
excludePatterns: [
|
||||
/^_/, // 私有方法
|
||||
/^get[A-Z]/, // getter 方法
|
||||
/^set[A-Z]/, // setter 方法
|
||||
/^is[A-Z]/, // 布尔检查方法
|
||||
/^has[A-Z]/, // 存在检查方法
|
||||
],
|
||||
maxBufferSize: 10000
|
||||
};
|
||||
|
||||
/**
|
||||
* 采样数据
|
||||
*/
|
||||
interface SampleData {
|
||||
timestamp: number;
|
||||
stack: string[];
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装信息
|
||||
*/
|
||||
interface WrapInfo {
|
||||
className: string;
|
||||
methodName: string;
|
||||
category: ProfileCategory;
|
||||
original: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动性能分析器
|
||||
*/
|
||||
export class AutoProfiler {
|
||||
private static _instance: AutoProfiler | null = null;
|
||||
private _config: AutoProfilerConfig;
|
||||
private wrappedObjects: WeakMap<object, Map<string, WrapInfo>> = new WeakMap();
|
||||
private samplingProfiler: SamplingProfiler | null = null;
|
||||
private registeredClasses: Map<string, { constructor: Function; category: ProfileCategory }> = new Map();
|
||||
|
||||
private constructor(config?: Partial<AutoProfilerConfig>) {
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取单例实例
|
||||
* @en Get singleton instance
|
||||
*/
|
||||
public static getInstance(config?: Partial<AutoProfilerConfig>): AutoProfiler {
|
||||
if (!AutoProfiler._instance) {
|
||||
AutoProfiler._instance = new AutoProfiler(config);
|
||||
}
|
||||
return AutoProfiler._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置实例
|
||||
* @en Reset instance
|
||||
*/
|
||||
public static resetInstance(): void {
|
||||
if (AutoProfiler._instance) {
|
||||
AutoProfiler._instance.dispose();
|
||||
AutoProfiler._instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用自动分析
|
||||
*/
|
||||
public static setEnabled(enabled: boolean): void {
|
||||
AutoProfiler.getInstance().setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册类以进行自动分析
|
||||
* 该类的所有实例方法都会被自动包装
|
||||
*/
|
||||
public static registerClass<T extends new (...args: any[]) => any>(
|
||||
constructor: T,
|
||||
category: ProfileCategory = ProfileCategory.Custom,
|
||||
className?: string
|
||||
): T {
|
||||
return AutoProfiler.getInstance().registerClass(constructor, category, className);
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装对象实例的所有方法
|
||||
*/
|
||||
public static wrapInstance<T extends object>(
|
||||
instance: T,
|
||||
className: string,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): T {
|
||||
return AutoProfiler.getInstance().wrapInstance(instance, className, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装单个函数
|
||||
*/
|
||||
public static wrapFunction<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
name: string,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): T {
|
||||
return AutoProfiler.getInstance().wrapFunction(fn, name, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动采样分析器
|
||||
*/
|
||||
public static startSampling(): void {
|
||||
AutoProfiler.getInstance().startSampling();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止采样分析器
|
||||
*/
|
||||
public static stopSampling(): SampleData[] {
|
||||
return AutoProfiler.getInstance().stopSampling();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置启用状态
|
||||
* @en Set enabled state
|
||||
*/
|
||||
public setEnabled(enabled: boolean): void {
|
||||
this._config.enabled = enabled;
|
||||
if (!enabled && this.samplingProfiler) {
|
||||
this.samplingProfiler.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注册类以进行自动分析
|
||||
* @en Register class for automatic profiling
|
||||
*/
|
||||
public registerClass<T extends new (...args: any[]) => any>(
|
||||
constructor: T,
|
||||
category: ProfileCategory = ProfileCategory.Custom,
|
||||
className?: string
|
||||
): T {
|
||||
const name = className || constructor.name;
|
||||
this.registeredClasses.set(name, { constructor, category });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for Proxy construct handler
|
||||
const self = this;
|
||||
|
||||
const ProxiedClass = new Proxy(constructor, {
|
||||
construct(target, args, newTarget) {
|
||||
const instance = Reflect.construct(target, args, newTarget);
|
||||
if (self._config.enabled) {
|
||||
self.wrapInstance(instance, name, category);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
});
|
||||
|
||||
return ProxiedClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 包装对象实例的所有方法
|
||||
* @en Wrap all methods of an object instance
|
||||
*/
|
||||
public wrapInstance<T extends object>(
|
||||
instance: T,
|
||||
className: string,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): T {
|
||||
if (!this._config.enabled) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 检查是否已经包装过
|
||||
if (this.wrappedObjects.has(instance)) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
const wrapInfoMap = new Map<string, WrapInfo>();
|
||||
this.wrappedObjects.set(instance, wrapInfoMap);
|
||||
|
||||
const methodNames = this._getAllMethodNames(instance);
|
||||
|
||||
for (const methodName of methodNames) {
|
||||
if (this._shouldExcludeMethod(methodName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const descriptor = this._getPropertyDescriptor(instance, methodName);
|
||||
if (!descriptor || typeof descriptor.value !== 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const original = descriptor.value as Function;
|
||||
const wrapped = this._createWrappedMethod(original, className, methodName, category);
|
||||
|
||||
wrapInfoMap.set(methodName, {
|
||||
className,
|
||||
methodName,
|
||||
category,
|
||||
original
|
||||
});
|
||||
|
||||
try {
|
||||
(instance as any)[methodName] = wrapped;
|
||||
} catch {
|
||||
// 某些属性可能是只读的
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 包装单个函数
|
||||
* @en Wrap a single function
|
||||
*/
|
||||
public wrapFunction<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
name: string,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): T {
|
||||
if (!this._config.enabled) return fn;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for wrapped function closure
|
||||
const self = this;
|
||||
|
||||
const wrapped = function(this: any, ...args: any[]): any {
|
||||
const handle = ProfilerSDK.beginSample(name, category);
|
||||
try {
|
||||
const result = fn.apply(this, args);
|
||||
|
||||
if (self._config.trackAsync && result instanceof Promise) {
|
||||
return result.finally(() => {
|
||||
ProfilerSDK.endSample(handle);
|
||||
});
|
||||
}
|
||||
|
||||
ProfilerSDK.endSample(handle);
|
||||
return result;
|
||||
} catch (error) {
|
||||
ProfilerSDK.endSample(handle);
|
||||
throw error;
|
||||
}
|
||||
} as T;
|
||||
|
||||
Object.defineProperty(wrapped, 'name', { value: fn.name || name });
|
||||
Object.defineProperty(wrapped, 'length', { value: fn.length });
|
||||
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 启动采样分析器
|
||||
* @en Start sampling profiler
|
||||
*/
|
||||
public startSampling(): void {
|
||||
if (!this.samplingProfiler) {
|
||||
this.samplingProfiler = new SamplingProfiler(this._config);
|
||||
}
|
||||
this.samplingProfiler.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止采样分析器
|
||||
*/
|
||||
public stopSampling(): SampleData[] {
|
||||
if (!this.samplingProfiler) {
|
||||
return [];
|
||||
}
|
||||
return this.samplingProfiler.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this.samplingProfiler) {
|
||||
this.samplingProfiler.stop();
|
||||
this.samplingProfiler = null;
|
||||
}
|
||||
this.registeredClasses.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建包装后的方法
|
||||
* @en Create wrapped method
|
||||
*/
|
||||
private _createWrappedMethod(
|
||||
original: Function,
|
||||
className: string,
|
||||
methodName: string,
|
||||
category: ProfileCategory
|
||||
): Function {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for wrapped method closure
|
||||
const self = this;
|
||||
const fullName = `${className}.${methodName}`;
|
||||
const minDuration = this._config.minDuration;
|
||||
|
||||
return function(this: any, ...args: any[]): any {
|
||||
if (!self._config.enabled || !ProfilerSDK.isEnabled()) {
|
||||
return original.apply(this, args);
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const handle = ProfilerSDK.beginSample(fullName, category);
|
||||
|
||||
try {
|
||||
const result = original.apply(this, args);
|
||||
|
||||
if (self._config.trackAsync && result instanceof Promise) {
|
||||
return result.then(
|
||||
(value) => {
|
||||
const duration = performance.now() - startTime;
|
||||
if (duration >= minDuration) {
|
||||
ProfilerSDK.endSample(handle);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
(error) => {
|
||||
ProfilerSDK.endSample(handle);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const duration = performance.now() - startTime;
|
||||
if (duration >= minDuration) {
|
||||
ProfilerSDK.endSample(handle);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
ProfilerSDK.endSample(handle);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取对象的所有方法名
|
||||
* @en Get all method names of an object
|
||||
*/
|
||||
private _getAllMethodNames(obj: object): string[] {
|
||||
const methods = new Set<string>();
|
||||
let current = obj;
|
||||
|
||||
while (current && current !== Object.prototype) {
|
||||
for (const name of Object.getOwnPropertyNames(current)) {
|
||||
if (name !== 'constructor') {
|
||||
methods.add(name);
|
||||
}
|
||||
}
|
||||
current = Object.getPrototypeOf(current);
|
||||
}
|
||||
|
||||
return Array.from(methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取属性描述符
|
||||
* @en Get property descriptor
|
||||
*/
|
||||
private _getPropertyDescriptor(obj: object, name: string): PropertyDescriptor | undefined {
|
||||
let current = obj;
|
||||
while (current && current !== Object.prototype) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(current, name);
|
||||
if (descriptor) return descriptor;
|
||||
current = Object.getPrototypeOf(current);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 判断是否应该排除该方法
|
||||
* @en Check if method should be excluded
|
||||
*/
|
||||
private _shouldExcludeMethod(methodName: string): boolean {
|
||||
if (methodName === 'constructor' || methodName.startsWith('__')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const pattern of this._config.excludePatterns) {
|
||||
if (pattern.test(methodName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 采样分析器
|
||||
* 使用定时器定期采样调用栈信息
|
||||
*/
|
||||
class SamplingProfiler {
|
||||
private config: AutoProfilerConfig;
|
||||
private samples: SampleData[] = [];
|
||||
private intervalId: number | null = null;
|
||||
private isRunning = false;
|
||||
|
||||
constructor(config: AutoProfilerConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始采样
|
||||
*/
|
||||
public start(): void {
|
||||
if (this.isRunning) return;
|
||||
|
||||
this.isRunning = true;
|
||||
this.samples = [];
|
||||
|
||||
// 使用 requestAnimationFrame 或 setInterval 进行采样
|
||||
const sample = () => {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
const stack = this.captureStack();
|
||||
if (stack.length > 0) {
|
||||
this.samples.push({
|
||||
timestamp: performance.now(),
|
||||
stack
|
||||
});
|
||||
|
||||
// 限制缓冲区大小
|
||||
if (this.samples.length > this.config.maxBufferSize) {
|
||||
this.samples.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// 继续采样
|
||||
if (this.config.sampleInterval < 16) {
|
||||
// 高频采样使用 setTimeout
|
||||
this.intervalId = setTimeout(sample, this.config.sampleInterval) as any;
|
||||
} else {
|
||||
this.intervalId = setTimeout(sample, this.config.sampleInterval) as any;
|
||||
}
|
||||
};
|
||||
|
||||
sample();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止采样并返回数据
|
||||
*/
|
||||
public stop(): SampleData[] {
|
||||
this.isRunning = false;
|
||||
if (this.intervalId !== null) {
|
||||
clearTimeout(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
return [...this.samples];
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获当前调用栈
|
||||
*/
|
||||
private captureStack(): string[] {
|
||||
try {
|
||||
// 创建 Error 对象获取调用栈
|
||||
const error = new Error();
|
||||
const stack = error.stack || '';
|
||||
|
||||
// 解析调用栈
|
||||
const lines = stack.split('\n').slice(3); // 跳过 Error 和 captureStack/sample
|
||||
const frames: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const frame = this.parseStackFrame(line);
|
||||
if (frame && !this.isInternalFrame(frame)) {
|
||||
frames.push(frame);
|
||||
}
|
||||
}
|
||||
|
||||
return frames;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析调用栈帧
|
||||
*/
|
||||
private parseStackFrame(line: string): string | null {
|
||||
// Chrome/Edge 格式: " at functionName (file:line:col)"
|
||||
// Firefox 格式: "functionName@file:line:col"
|
||||
// Safari 格式: "functionName@file:line:col"
|
||||
|
||||
line = line.trim();
|
||||
|
||||
// Chrome 格式
|
||||
let match = line.match(/at\s+(.+?)\s+\(/);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
// Chrome 匿名函数格式
|
||||
match = line.match(/at\s+(.+)/);
|
||||
if (match && match[1]) {
|
||||
const name = match[1];
|
||||
if (!name.includes('(')) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox/Safari 格式
|
||||
match = line.match(/^(.+?)@/);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是内部帧(应该过滤掉)
|
||||
*/
|
||||
private isInternalFrame(frame: string): boolean {
|
||||
const internalPatterns = [
|
||||
'SamplingProfiler',
|
||||
'AutoProfiler',
|
||||
'ProfilerSDK',
|
||||
'setTimeout',
|
||||
'setInterval',
|
||||
'requestAnimationFrame',
|
||||
'<anonymous>',
|
||||
'eval'
|
||||
];
|
||||
|
||||
return internalPatterns.some(pattern => frame.includes(pattern));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Profile 装饰器
|
||||
* 用于标记需要性能分析的方法
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class MySystem extends System {
|
||||
* @Profile()
|
||||
* update() {
|
||||
* // 方法执行时间会被自动记录
|
||||
* }
|
||||
*
|
||||
* @Profile('customName', ProfileCategory.Physics)
|
||||
* calculatePhysics() {
|
||||
* // 使用自定义名称和分类
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Profile(
|
||||
name?: string,
|
||||
category: ProfileCategory = ProfileCategory.Custom
|
||||
): MethodDecorator {
|
||||
return function(
|
||||
target: object,
|
||||
propertyKey: string | symbol,
|
||||
descriptor: PropertyDescriptor
|
||||
): PropertyDescriptor {
|
||||
const original = descriptor.value;
|
||||
const methodName = name || `${target.constructor.name}.${String(propertyKey)}`;
|
||||
|
||||
descriptor.value = function(this: any, ...args: any[]): any {
|
||||
if (!ProfilerSDK.isEnabled()) {
|
||||
return original.apply(this, args);
|
||||
}
|
||||
|
||||
const handle = ProfilerSDK.beginSample(methodName, category);
|
||||
try {
|
||||
const result = original.apply(this, args);
|
||||
|
||||
// 处理异步方法
|
||||
if (result instanceof Promise) {
|
||||
return result.finally(() => {
|
||||
ProfilerSDK.endSample(handle);
|
||||
});
|
||||
}
|
||||
|
||||
// 同步方法,立即结束采样
|
||||
ProfilerSDK.endSample(handle);
|
||||
return result;
|
||||
} catch (error) {
|
||||
// 发生错误时也要结束采样
|
||||
ProfilerSDK.endSample(handle);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @ProfileClass 装饰器
|
||||
* 用于自动包装类的所有方法
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @ProfileClass(ProfileCategory.Physics)
|
||||
* class PhysicsSystem extends System {
|
||||
* update() { ... } // 自动被包装
|
||||
* calculate() { ... } // 自动被包装
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function ProfileClass(category: ProfileCategory = ProfileCategory.Custom): ClassDecorator {
|
||||
return function<T extends Function>(constructor: T): T {
|
||||
return AutoProfiler.registerClass(constructor as any, category) as any;
|
||||
};
|
||||
}
|
||||
901
packages/framework/core/src/Utils/Profiler/ProfilerSDK.ts
Normal file
901
packages/framework/core/src/Utils/Profiler/ProfilerSDK.ts
Normal file
@@ -0,0 +1,901 @@
|
||||
/**
|
||||
* 性能分析器 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取单例实例
|
||||
* @en Get singleton instance
|
||||
*/
|
||||
public static getInstance(config?: Partial<ProfilerConfig>): ProfilerSDK {
|
||||
if (!ProfilerSDK._instance) {
|
||||
ProfilerSDK._instance = new ProfilerSDK(config);
|
||||
}
|
||||
return ProfilerSDK._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置实例(测试用)
|
||||
* @en Reset instance (for testing)
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否启用
|
||||
* @en Check if 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 开始采样
|
||||
* @en Begin sample
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 结束采样
|
||||
* @en End sample
|
||||
*/
|
||||
public endSample(handle: SampleHandle): void {
|
||||
if (!this._config.enabled || !this.activeSamples.has(handle.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - handle.startTime;
|
||||
|
||||
// 获取父级 handle(在删除当前 handle 之前)
|
||||
const parentHandle = handle.parentId ? this.activeSamples.get(handle.parentId) : undefined;
|
||||
|
||||
const sample: ProfileSample = {
|
||||
id: handle.id,
|
||||
name: handle.name,
|
||||
category: handle.category,
|
||||
startTime: handle.startTime,
|
||||
endTime,
|
||||
duration,
|
||||
selfTime: duration,
|
||||
parentId: handle.parentId,
|
||||
parentName: parentHandle?.name,
|
||||
depth: handle.depth,
|
||||
callCount: 1
|
||||
};
|
||||
|
||||
if (this.currentFrame) {
|
||||
this.currentFrame.samples.push(sample);
|
||||
}
|
||||
|
||||
this._updateCallGraph(handle.name, handle.category, duration, parentHandle);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 开始帧
|
||||
* @en Begin frame
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 结束帧
|
||||
* @en End frame
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 递增计数器
|
||||
* @en Increment counter
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置仪表值
|
||||
* @en Set gauge value
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置启用状态
|
||||
* @en Set enabled state
|
||||
*/
|
||||
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 callGraph = this._buildCallGraphFromFrames(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,
|
||||
categoryBreakdown,
|
||||
memoryTrend: frames.map((f) => f.memory),
|
||||
longTasks: [...this.longTasks]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从帧历史构建调用图
|
||||
* @en Build call graph from frame history
|
||||
*
|
||||
* @zh 注意:totalTime 存储的是平均耗时(总耗时/调用次数),而不是累计总耗时
|
||||
* @en Note: totalTime stores average time (total/count), not cumulative total
|
||||
*/
|
||||
private _buildCallGraphFromFrames(frames: ProfileFrame[]): Map<string, CallGraphNode> {
|
||||
// 临时存储累计数据
|
||||
const tempData = new Map<string, {
|
||||
category: ProfileCategory;
|
||||
callCount: number;
|
||||
totalTime: number;
|
||||
callers: Map<string, { count: number; totalTime: number }>;
|
||||
callees: Map<string, { count: number; totalTime: number }>;
|
||||
}>();
|
||||
|
||||
for (const frame of frames) {
|
||||
for (const sample of frame.samples) {
|
||||
// 获取或创建当前函数的节点
|
||||
let node = tempData.get(sample.name);
|
||||
if (!node) {
|
||||
node = {
|
||||
category: sample.category,
|
||||
callCount: 0,
|
||||
totalTime: 0,
|
||||
callers: new Map(),
|
||||
callees: new Map()
|
||||
};
|
||||
tempData.set(sample.name, node);
|
||||
}
|
||||
|
||||
node.callCount++;
|
||||
node.totalTime += sample.duration;
|
||||
|
||||
// 如果有父级,建立调用关系
|
||||
if (sample.parentName) {
|
||||
// 记录当前函数被谁调用
|
||||
const callerData = node.callers.get(sample.parentName) || { count: 0, totalTime: 0 };
|
||||
callerData.count++;
|
||||
callerData.totalTime += sample.duration;
|
||||
node.callers.set(sample.parentName, callerData);
|
||||
|
||||
// 确保父节点存在并记录它调用了谁
|
||||
let parentNode = tempData.get(sample.parentName);
|
||||
if (!parentNode) {
|
||||
parentNode = {
|
||||
category: sample.category,
|
||||
callCount: 0,
|
||||
totalTime: 0,
|
||||
callers: new Map(),
|
||||
callees: new Map()
|
||||
};
|
||||
tempData.set(sample.parentName, parentNode);
|
||||
}
|
||||
|
||||
const calleeData = parentNode.callees.get(sample.name) || { count: 0, totalTime: 0 };
|
||||
calleeData.count++;
|
||||
calleeData.totalTime += sample.duration;
|
||||
parentNode.callees.set(sample.name, calleeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为最终结果,将 totalTime 改为平均耗时
|
||||
const callGraph = new Map<string, CallGraphNode>();
|
||||
for (const [name, data] of tempData) {
|
||||
const avgCallers = new Map<string, { count: number; totalTime: number }>();
|
||||
for (const [callerName, callerData] of data.callers) {
|
||||
avgCallers.set(callerName, {
|
||||
count: callerData.count,
|
||||
totalTime: callerData.count > 0 ? callerData.totalTime / callerData.count : 0
|
||||
});
|
||||
}
|
||||
|
||||
const avgCallees = new Map<string, { count: number; totalTime: number }>();
|
||||
for (const [calleeName, calleeData] of data.callees) {
|
||||
avgCallees.set(calleeName, {
|
||||
count: calleeData.count,
|
||||
totalTime: calleeData.count > 0 ? calleeData.totalTime / calleeData.count : 0
|
||||
});
|
||||
}
|
||||
|
||||
callGraph.set(name, {
|
||||
name,
|
||||
category: data.category,
|
||||
callCount: data.callCount,
|
||||
totalTime: data.callCount > 0 ? data.totalTime / data.callCount : 0,
|
||||
callers: avgCallers,
|
||||
callees: avgCallees
|
||||
});
|
||||
}
|
||||
|
||||
return callGraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用图数据
|
||||
*/
|
||||
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,
|
||||
parentHandle?: SampleHandle
|
||||
): 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 (parentHandle) {
|
||||
// 记录当前函数被谁调用
|
||||
const callerData = node.callers.get(parentHandle.name) || { count: 0, totalTime: 0 };
|
||||
callerData.count++;
|
||||
callerData.totalTime += duration;
|
||||
node.callers.set(parentHandle.name, callerData);
|
||||
|
||||
// 确保父节点存在
|
||||
let parentNode = this.callGraph.get(parentHandle.name);
|
||||
if (!parentNode) {
|
||||
parentNode = {
|
||||
name: parentHandle.name,
|
||||
category: parentHandle.category,
|
||||
callCount: 0,
|
||||
totalTime: 0,
|
||||
callers: new Map(),
|
||||
callees: new Map()
|
||||
};
|
||||
this.callGraph.set(parentHandle.name, 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: []
|
||||
};
|
||||
}
|
||||
}
|
||||
229
packages/framework/core/src/Utils/Profiler/ProfilerTypes.ts
Normal file
229
packages/framework/core/src/Utils/Profiler/ProfilerTypes.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* 性能分析器类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 性能分析类别
|
||||
*/
|
||||
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 type SampleHandle = {
|
||||
id: string;
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
startTime: number;
|
||||
depth: number;
|
||||
parentId?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能采样数据
|
||||
*/
|
||||
export type ProfileSample = {
|
||||
id: string;
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
selfTime: number;
|
||||
parentId?: string | undefined;
|
||||
/** 父级采样的名称(用于构建调用图) */
|
||||
parentName?: string | undefined;
|
||||
depth: number;
|
||||
callCount: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合后的采样统计
|
||||
*/
|
||||
export type 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 type MemorySnapshot = {
|
||||
timestamp: number;
|
||||
/** 已使用堆内存 (bytes) */
|
||||
usedHeapSize: number;
|
||||
/** 总堆内存 (bytes) */
|
||||
totalHeapSize: number;
|
||||
/** 堆内存限制 (bytes) */
|
||||
heapSizeLimit: number;
|
||||
/** 使用率 (0-100) */
|
||||
utilizationPercent: number;
|
||||
/** 检测到的 GC 次数 */
|
||||
gcCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数器数据
|
||||
*/
|
||||
export type ProfileCounter = {
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
value: number;
|
||||
type: 'counter' | 'gauge';
|
||||
history: Array<{ time: number; value: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单帧性能数据
|
||||
*/
|
||||
export type 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 type ProfilerConfig = {
|
||||
/** 是否启用 */
|
||||
enabled: boolean;
|
||||
/** 最大历史帧数 */
|
||||
maxFrameHistory: number;
|
||||
/** 采样深度限制 */
|
||||
maxSampleDepth: number;
|
||||
/** 是否收集内存数据 */
|
||||
collectMemory: boolean;
|
||||
/** 内存采样间隔 (ms) */
|
||||
memorySampleInterval: number;
|
||||
/** 是否检测长任务 */
|
||||
detectLongTasks: boolean;
|
||||
/** 长任务阈值 (ms) */
|
||||
longTaskThreshold: number;
|
||||
/** 启用的类别 */
|
||||
enabledCategories: Set<ProfileCategory>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 长任务信息
|
||||
*/
|
||||
export type LongTaskInfo = {
|
||||
startTime: number;
|
||||
duration: number;
|
||||
attribution: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用关系节点
|
||||
*/
|
||||
export type CallGraphNode = {
|
||||
name: string;
|
||||
category: ProfileCategory;
|
||||
/** 被调用次数 */
|
||||
callCount: number;
|
||||
/** 总耗时 */
|
||||
totalTime: number;
|
||||
/** 调用者列表 */
|
||||
callers: Map<string, { count: number; totalTime: number }>;
|
||||
/** 被调用者列表 */
|
||||
callees: Map<string, { count: number; totalTime: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能分析报告
|
||||
*/
|
||||
export type 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))
|
||||
};
|
||||
8
packages/framework/core/src/Utils/Profiler/index.ts
Normal file
8
packages/framework/core/src/Utils/Profiler/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 性能分析器模块
|
||||
*/
|
||||
|
||||
export * from './ProfilerTypes';
|
||||
export { ProfilerSDK } from './ProfilerSDK';
|
||||
export { AutoProfiler, Profile, ProfileClass } from './AutoProfiler';
|
||||
export type { AutoProfilerConfig } from './AutoProfiler';
|
||||
73
packages/framework/core/src/Utils/Time.ts
Normal file
73
packages/framework/core/src/Utils/Time.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 时间管理工具类
|
||||
* 提供游戏时间相关的功能,包括帧时间、总时间、时间缩放等
|
||||
*/
|
||||
export class Time {
|
||||
/**
|
||||
* 上一帧到当前帧的时间间隔(秒)
|
||||
*/
|
||||
public static deltaTime: number = 0;
|
||||
|
||||
/**
|
||||
* 未缩放的帧时间间隔(秒)
|
||||
*/
|
||||
public static unscaledDeltaTime: number = 0;
|
||||
|
||||
/**
|
||||
* 游戏开始以来的总时间(秒)
|
||||
*/
|
||||
public static totalTime: number = 0;
|
||||
|
||||
/**
|
||||
* 未缩放的总时间(秒)
|
||||
*/
|
||||
public static unscaledTotalTime: number = 0;
|
||||
|
||||
/**
|
||||
* 时间缩放比例
|
||||
*/
|
||||
public static timeScale: number = 1;
|
||||
|
||||
/**
|
||||
* 当前帧数
|
||||
*/
|
||||
public static frameCount: number = 0;
|
||||
|
||||
/**
|
||||
* 使用外部引擎提供的deltaTime更新时间信息
|
||||
* @param deltaTime 外部引擎提供的帧时间间隔(秒)
|
||||
*/
|
||||
public static update(deltaTime: number): void {
|
||||
// 设置未缩放的帧时间
|
||||
this.unscaledDeltaTime = deltaTime;
|
||||
this.deltaTime = deltaTime * this.timeScale;
|
||||
|
||||
// 更新总时间
|
||||
this.unscaledTotalTime += this.unscaledDeltaTime;
|
||||
this.totalTime += this.deltaTime;
|
||||
|
||||
// 更新帧数
|
||||
this.frameCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景改变时重置时间
|
||||
*/
|
||||
public static sceneChanged(): void {
|
||||
this.frameCount = 0;
|
||||
this.totalTime = 0;
|
||||
this.unscaledTotalTime = 0;
|
||||
this.deltaTime = 0;
|
||||
this.unscaledDeltaTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定的时间间隔是否已经过去
|
||||
* @param interval 时间间隔(秒)
|
||||
* @param lastTime 上次检查的时间
|
||||
* @returns 是否已经过去指定时间
|
||||
*/
|
||||
public static checkEvery(interval: number, lastTime: number): boolean {
|
||||
return this.totalTime - lastTime >= interval;
|
||||
}
|
||||
}
|
||||
18
packages/framework/core/src/Utils/Timers/ITimer.ts
Normal file
18
packages/framework/core/src/Utils/Timers/ITimer.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type ITimer<TContext = unknown> = {
|
||||
context: TContext;
|
||||
|
||||
/**
|
||||
* 调用stop以停止此计时器再次运行。这对非重复计时器没有影响。
|
||||
*/
|
||||
stop(): void;
|
||||
|
||||
/**
|
||||
* 将计时器的运行时间重置为0
|
||||
*/
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* 返回投向T的上下文,作为方便
|
||||
*/
|
||||
getContext<T>(): T;
|
||||
}
|
||||
70
packages/framework/core/src/Utils/Timers/Timer.ts
Normal file
70
packages/framework/core/src/Utils/Timers/Timer.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ITimer } from './ITimer';
|
||||
import { Time } from '../Time';
|
||||
|
||||
/**
|
||||
* 私有类隐藏ITimer的实现
|
||||
*/
|
||||
export class Timer<TContext = unknown> implements ITimer<TContext>{
|
||||
public context!: TContext;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时器是否已完成
|
||||
*/
|
||||
public get isDone(): boolean {
|
||||
return this._isDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时器已运行的时间
|
||||
*/
|
||||
public get elapsedTime(): number {
|
||||
return this._elapsedTime;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._elapsedTime = 0;
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this._isDone = true;
|
||||
}
|
||||
|
||||
public tick(){
|
||||
// 如果stop在tick之前被调用,那么isDone将为true,我们不应该再做任何事情
|
||||
if (!this._isDone && this._elapsedTime > this._timeInSeconds){
|
||||
this._elapsedTime -= this._timeInSeconds;
|
||||
this._onTime(this);
|
||||
|
||||
if (!this._isDone && !this._repeats)
|
||||
this._isDone = true;
|
||||
}
|
||||
|
||||
this._elapsedTime += Time.deltaTime;
|
||||
|
||||
return this._isDone;
|
||||
}
|
||||
|
||||
public initialize(timeInsSeconds: number, repeats: boolean, context: TContext, onTime: (timer: ITimer<TContext>)=>void){
|
||||
this._timeInSeconds = timeInsSeconds;
|
||||
this._repeats = repeats;
|
||||
this.context = context;
|
||||
this._onTime = onTime.bind(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 空出对象引用,以便在js需要时GC可以清理它们的引用
|
||||
*/
|
||||
public unload(){
|
||||
this.context = null as unknown as TContext;
|
||||
this._onTime = null!;
|
||||
}
|
||||
}
|
||||
49
packages/framework/core/src/Utils/Timers/TimerManager.ts
Normal file
49
packages/framework/core/src/Utils/Timers/TimerManager.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Timer } from './Timer';
|
||||
import { ITimer } from './ITimer';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
import type { IUpdatable } from '../../Types/IUpdatable';
|
||||
import { Updatable } from '../../Core/DI';
|
||||
|
||||
/**
|
||||
* 定时器管理器
|
||||
*
|
||||
* 允许动作的延迟和重复执行
|
||||
*/
|
||||
@Updatable()
|
||||
export class TimerManager implements IService, IUpdatable {
|
||||
private _timers: Array<Timer<unknown>> = [];
|
||||
|
||||
public update() {
|
||||
for (let i = this._timers.length - 1; i >= 0; i --){
|
||||
if (this._timers[i]!.tick()){
|
||||
this._timers[i]!.unload();
|
||||
this._timers.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度一个一次性或重复的计时器,该计时器将调用已传递的动作
|
||||
* @param timeInSeconds
|
||||
* @param repeats
|
||||
* @param context
|
||||
* @param onTime
|
||||
*/
|
||||
public schedule<TContext = unknown>(timeInSeconds: number, repeats: boolean, context: TContext, onTime: (timer: ITimer<TContext>)=>void): Timer<TContext> {
|
||||
const timer = new Timer<TContext>();
|
||||
timer.initialize(timeInSeconds, repeats, context, onTime);
|
||||
this._timers.push(timer as Timer<unknown>);
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
for (const timer of this._timers) {
|
||||
timer.unload();
|
||||
}
|
||||
this._timers = [];
|
||||
}
|
||||
}
|
||||
11
packages/framework/core/src/Utils/index.ts
Normal file
11
packages/framework/core/src/Utils/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from './Extensions';
|
||||
export * from './Pool';
|
||||
export * from './Emitter';
|
||||
export * from './GlobalManager';
|
||||
export * from './PerformanceMonitor';
|
||||
export { Time } from './Time';
|
||||
export * from './Debug';
|
||||
export * from './Logger';
|
||||
export * from './BinarySerializer';
|
||||
export * from './Profiler';
|
||||
export * from './GUID';
|
||||
Reference in New Issue
Block a user