271 lines
9.7 KiB
TypeScript
271 lines
9.7 KiB
TypeScript
import { IComponentDebugData } from '../../Types';
|
|
import { Core } from '../../Core';
|
|
|
|
/**
|
|
* 组件数据收集器
|
|
*/
|
|
export class ComponentDataCollector {
|
|
private static componentSizeCache = new Map<string, number>();
|
|
|
|
/**
|
|
* 收集组件数据(轻量版,不计算实际内存大小)
|
|
*/
|
|
public collectComponentData(): IComponentDebugData {
|
|
const scene = Core.scene;
|
|
if (!scene) {
|
|
return {
|
|
componentTypes: 0,
|
|
componentInstances: 0,
|
|
componentStats: []
|
|
};
|
|
}
|
|
|
|
const entityList = (scene as any).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 = component.constructor.name;
|
|
const stats = componentStats.get(typeName) || { count: 0, entities: 0 };
|
|
stats.count++;
|
|
totalInstances++;
|
|
componentStats.set(typeName, stats);
|
|
});
|
|
}
|
|
});
|
|
|
|
// 获取池利用率信息
|
|
let poolUtilizations = new Map<string, number>();
|
|
let poolSizes = new Map<string, number>();
|
|
|
|
try {
|
|
const { ComponentPoolManager } = require('../../ECS/Core/ComponentPool');
|
|
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);
|
|
|
|
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): number {
|
|
if (ComponentDataCollector.componentSizeCache.has(typeName)) {
|
|
return ComponentDataCollector.componentSizeCache.get(typeName)!;
|
|
}
|
|
|
|
const scene = Core.scene;
|
|
if (!scene) return 64;
|
|
|
|
const entityList = (scene as any).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) => c.constructor.name === 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 === '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);
|
|
}
|
|
|
|
/**
|
|
* 为内存快照功能提供的详细内存计算
|
|
* 只在用户主动请求内存快照时调用
|
|
*/
|
|
public calculateDetailedComponentMemory(typeName: string): number {
|
|
const scene = Core.scene;
|
|
if (!scene) return this.getEstimatedComponentSize(typeName);
|
|
|
|
const entityList = (scene as any).entities;
|
|
if (!entityList?.buffer) return this.getEstimatedComponentSize(typeName);
|
|
|
|
try {
|
|
// 找到第一个包含此组件的实体,分析组件大小
|
|
for (const entity of entityList.buffer) {
|
|
if (entity.components) {
|
|
const component = entity.components.find((c: any) => c.constructor.name === typeName);
|
|
if (component) {
|
|
return this.estimateObjectSize(component);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// 忽略错误,使用估算值
|
|
}
|
|
|
|
return this.getEstimatedComponentSize(typeName);
|
|
}
|
|
|
|
/**
|
|
* 估算对象内存大小(仅用于内存快照)
|
|
* 优化版本:减少递归深度,提高性能
|
|
*/
|
|
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 === '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();
|
|
}
|
|
}
|