集成tsrpc代替protobuf
This commit is contained in:
@@ -1,445 +0,0 @@
|
||||
/**
|
||||
* Protobuf序列化性能测试
|
||||
*/
|
||||
|
||||
import { Component, Entity, Scene } from '@esengine/ecs-framework';
|
||||
import { SnapshotManager } from '../../src/Snapshot/SnapshotManager';
|
||||
import { ProtobufSerializer } from '../../src/Serialization/ProtobufSerializer';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool
|
||||
} from '../../src/Serialization/ProtobufDecorators';
|
||||
|
||||
// 性能测试组件
|
||||
@ProtoSerializable('PerfPosition')
|
||||
class PerfPositionComponent extends Component {
|
||||
@ProtoFloat(1) public x: number = 0;
|
||||
@ProtoFloat(2) public y: number = 0;
|
||||
@ProtoFloat(3) public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('PerfVelocity')
|
||||
class PerfVelocityComponent extends Component {
|
||||
@ProtoFloat(1) public vx: number = 0;
|
||||
@ProtoFloat(2) public vy: number = 0;
|
||||
@ProtoFloat(3) public vz: number = 0;
|
||||
|
||||
constructor(vx: number = 0, vy: number = 0, vz: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
this.vz = vz;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('PerfHealth')
|
||||
class PerfHealthComponent extends Component {
|
||||
@ProtoInt32(1) public maxHealth: number = 100;
|
||||
@ProtoInt32(2) public currentHealth: number = 100;
|
||||
@ProtoBool(3) public isDead: boolean = false;
|
||||
@ProtoFloat(4) public regenerationRate: number = 0.5;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('PerfPlayer')
|
||||
class PerfPlayerComponent extends Component {
|
||||
@ProtoString(1) public name: string = '';
|
||||
@ProtoInt32(2) public level: number = 1;
|
||||
@ProtoInt32(3) public experience: number = 0;
|
||||
@ProtoInt32(4) public score: number = 0;
|
||||
@ProtoBool(5) public isOnline: boolean = true;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
// 传统JSON序列化组件(用于对比)
|
||||
class JsonPositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
class JsonPlayerComponent extends Component {
|
||||
public name: string = '';
|
||||
public level: number = 1;
|
||||
public experience: number = 0;
|
||||
public score: number = 0;
|
||||
public isOnline: boolean = true;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
// Mock protobuf.js for performance testing
|
||||
const createMockProtobuf = () => {
|
||||
const mockEncodedData = new Uint8Array(32);
|
||||
mockEncodedData.fill(1);
|
||||
|
||||
return {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
root: {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||
// 根据类型名返回相应的数据
|
||||
const mockData: Record<string, any> = {
|
||||
'ecs.PerfPosition': { x: 10, y: 20, z: 30 },
|
||||
'ecs.PerfVelocity': { vx: 1, vy: 2, vz: 3 },
|
||||
'ecs.PerfHealth': { maxHealth: 100, currentHealth: 80, isDead: false, regenerationRate: 0.5 },
|
||||
'ecs.PerfPlayer': { name: 'TestPlayer', level: 5, experience: 1000, score: 5000, isOnline: true }
|
||||
};
|
||||
|
||||
return {
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(mockEncodedData)
|
||||
}),
|
||||
decode: jest.fn().mockReturnValue(mockData[typeName] || {}),
|
||||
toObject: jest.fn().mockImplementation((message) => message)
|
||||
};
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
describe('Protobuf序列化性能测试', () => {
|
||||
let protobufSerializer: ProtobufSerializer;
|
||||
let snapshotManager: SnapshotManager;
|
||||
let scene: Scene;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockProtobuf = createMockProtobuf();
|
||||
protobufSerializer = ProtobufSerializer.getInstance();
|
||||
protobufSerializer.initialize(mockProtobuf.parse().root as any);
|
||||
|
||||
snapshotManager = new SnapshotManager();
|
||||
snapshotManager.initializeProtobuf(mockProtobuf.parse().root as any);
|
||||
|
||||
scene = new Scene();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('单组件序列化性能', () => {
|
||||
const iterations = 1000;
|
||||
|
||||
it('应该比较protobuf和JSON序列化速度', () => {
|
||||
const protobufComponents: PerfPositionComponent[] = [];
|
||||
const jsonComponents: JsonPositionComponent[] = [];
|
||||
|
||||
// 准备测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
protobufComponents.push(new PerfPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
|
||||
jsonComponents.push(new JsonPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
}
|
||||
|
||||
// 测试Protobuf序列化
|
||||
const protobufStartTime = performance.now();
|
||||
let protobufTotalSize = 0;
|
||||
|
||||
for (const component of protobufComponents) {
|
||||
const result = protobufSerializer.serialize(component);
|
||||
protobufTotalSize += result.size;
|
||||
}
|
||||
|
||||
const protobufEndTime = performance.now();
|
||||
const protobufTime = protobufEndTime - protobufStartTime;
|
||||
|
||||
// 测试JSON序列化
|
||||
const jsonStartTime = performance.now();
|
||||
let jsonTotalSize = 0;
|
||||
|
||||
for (const component of jsonComponents) {
|
||||
const jsonString = JSON.stringify({
|
||||
x: component.x,
|
||||
y: component.y,
|
||||
z: component.z
|
||||
});
|
||||
jsonTotalSize += new Blob([jsonString]).size;
|
||||
}
|
||||
|
||||
const jsonEndTime = performance.now();
|
||||
const jsonTime = jsonEndTime - jsonStartTime;
|
||||
|
||||
// 性能断言
|
||||
console.log(`\\n=== 单组件序列化性能对比 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`);
|
||||
console.log(`Protobuf总大小: ${protobufTotalSize} bytes`);
|
||||
console.log(`JSON总大小: ${jsonTotalSize} bytes`);
|
||||
|
||||
if (jsonTime > 0) {
|
||||
const speedImprovement = ((jsonTime - protobufTime) / jsonTime * 100);
|
||||
console.log(`速度提升: ${speedImprovement.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
if (jsonTotalSize > 0) {
|
||||
const sizeReduction = ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100);
|
||||
console.log(`大小减少: ${sizeReduction.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
// 基本性能验证
|
||||
expect(protobufTime).toBeLessThan(1000); // 不应该超过1秒
|
||||
expect(jsonTime).toBeLessThan(1000);
|
||||
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该测试复杂组件的序列化性能', () => {
|
||||
const protobufPlayers: PerfPlayerComponent[] = [];
|
||||
const jsonPlayers: JsonPlayerComponent[] = [];
|
||||
|
||||
// 创建测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
protobufPlayers.push(new PerfPlayerComponent(
|
||||
`Player${i}`,
|
||||
Math.floor(Math.random() * 100) + 1
|
||||
));
|
||||
|
||||
jsonPlayers.push(new JsonPlayerComponent(
|
||||
`Player${i}`,
|
||||
Math.floor(Math.random() * 100) + 1
|
||||
));
|
||||
}
|
||||
|
||||
// Protobuf序列化测试
|
||||
const protobufStart = performance.now();
|
||||
for (const player of protobufPlayers) {
|
||||
protobufSerializer.serialize(player);
|
||||
}
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON序列化测试
|
||||
const jsonStart = performance.now();
|
||||
for (const player of jsonPlayers) {
|
||||
JSON.stringify({
|
||||
name: player.name,
|
||||
level: player.level,
|
||||
experience: player.experience,
|
||||
score: player.score,
|
||||
isOnline: player.isOnline
|
||||
});
|
||||
}
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
console.log(`\\n=== 复杂组件序列化性能 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`);
|
||||
|
||||
expect(protobufTime).toBeLessThan(1000);
|
||||
expect(jsonTime).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量实体序列化性能', () => {
|
||||
it('应该测试大量实体的快照创建性能', () => {
|
||||
const entityCount = 100;
|
||||
const entities: Entity[] = [];
|
||||
|
||||
// 创建测试实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`Entity${i}`);
|
||||
entity.addComponent(new PerfPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
entity.addComponent(new PerfVelocityComponent(
|
||||
Math.random() * 10 - 5,
|
||||
Math.random() * 10 - 5,
|
||||
Math.random() * 2 - 1
|
||||
));
|
||||
entity.addComponent(new PerfHealthComponent(100 + Math.floor(Math.random() * 50)));
|
||||
entity.addComponent(new PerfPlayerComponent(`Player${i}`, Math.floor(Math.random() * 50) + 1));
|
||||
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
// 测试快照创建性能
|
||||
const snapshotStart = performance.now();
|
||||
const snapshot = snapshotManager.createSceneSnapshot(entities);
|
||||
const snapshotTime = performance.now() - snapshotStart;
|
||||
|
||||
console.log(`\\n=== 批量实体序列化性能 ===`);
|
||||
console.log(`实体数量: ${entityCount}`);
|
||||
console.log(`每个实体组件数: 4`);
|
||||
console.log(`总组件数: ${entityCount * 4}`);
|
||||
console.log(`快照创建时间: ${snapshotTime.toFixed(2)}ms`);
|
||||
console.log(`平均每组件时间: ${(snapshotTime / (entityCount * 4)).toFixed(3)}ms`);
|
||||
|
||||
expect(snapshot.entities).toHaveLength(entityCount);
|
||||
expect(snapshotTime).toBeLessThan(5000); // 不应该超过5秒
|
||||
|
||||
// 计算快照大小
|
||||
let totalSnapshotSize = 0;
|
||||
for (const entitySnapshot of snapshot.entities) {
|
||||
for (const componentSnapshot of entitySnapshot.components) {
|
||||
if (componentSnapshot.data && typeof componentSnapshot.data === 'object' && 'size' in componentSnapshot.data) {
|
||||
totalSnapshotSize += (componentSnapshot.data as any).size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`快照总大小: ${totalSnapshotSize} bytes`);
|
||||
console.log(`平均每实体大小: ${(totalSnapshotSize / entityCount).toFixed(1)} bytes`);
|
||||
|
||||
expect(totalSnapshotSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('反序列化性能', () => {
|
||||
it('应该测试快照恢复性能', () => {
|
||||
const entityCount = 50;
|
||||
const originalEntities: Entity[] = [];
|
||||
|
||||
// 创建原始实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`Original${i}`);
|
||||
entity.addComponent(new PerfPositionComponent(i * 10, i * 20, i));
|
||||
entity.addComponent(new PerfHealthComponent(100 + i));
|
||||
originalEntities.push(entity);
|
||||
}
|
||||
|
||||
// 创建快照
|
||||
const snapshotStart = performance.now();
|
||||
const snapshot = snapshotManager.createSceneSnapshot(originalEntities);
|
||||
const snapshotTime = performance.now() - snapshotStart;
|
||||
|
||||
// 创建目标实体
|
||||
const targetEntities: Entity[] = [];
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`Target${i}`);
|
||||
entity.addComponent(new PerfPositionComponent());
|
||||
entity.addComponent(new PerfHealthComponent());
|
||||
targetEntities.push(entity);
|
||||
}
|
||||
|
||||
// 测试恢复性能
|
||||
const restoreStart = performance.now();
|
||||
snapshotManager.restoreFromSnapshot(snapshot, targetEntities);
|
||||
const restoreTime = performance.now() - restoreStart;
|
||||
|
||||
console.log(`\\n=== 反序列化性能测试 ===`);
|
||||
console.log(`实体数量: ${entityCount}`);
|
||||
console.log(`序列化时间: ${snapshotTime.toFixed(2)}ms`);
|
||||
console.log(`反序列化时间: ${restoreTime.toFixed(2)}ms`);
|
||||
console.log(`总往返时间: ${(snapshotTime + restoreTime).toFixed(2)}ms`);
|
||||
console.log(`平均每实体往返时间: ${((snapshotTime + restoreTime) / entityCount).toFixed(3)}ms`);
|
||||
|
||||
expect(restoreTime).toBeLessThan(2000); // 不应该超过2秒
|
||||
expect(snapshotTime + restoreTime).toBeLessThan(3000); // 总时间不超过3秒
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存使用', () => {
|
||||
it('应该监控序列化过程中的内存使用', () => {
|
||||
const entityCount = 200;
|
||||
const entities: Entity[] = [];
|
||||
|
||||
// 创建大量实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`MemoryTest${i}`);
|
||||
entity.addComponent(new PerfPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
entity.addComponent(new PerfVelocityComponent(
|
||||
Math.random() * 10,
|
||||
Math.random() * 10,
|
||||
Math.random() * 2
|
||||
));
|
||||
entity.addComponent(new PerfHealthComponent(Math.floor(Math.random() * 200) + 50));
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
// 记录初始内存(如果可用)
|
||||
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||
|
||||
// 执行序列化
|
||||
const snapshot = snapshotManager.createSceneSnapshot(entities);
|
||||
|
||||
// 记录序列化后内存
|
||||
const afterMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||
const memoryIncrease = afterMemory - initialMemory;
|
||||
|
||||
if (initialMemory > 0) {
|
||||
console.log(`\\n=== 内存使用测试 ===`);
|
||||
console.log(`实体数量: ${entityCount}`);
|
||||
console.log(`初始内存: ${(initialMemory / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`序列化后内存: ${(afterMemory / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`内存增加: ${(memoryIncrease / 1024).toFixed(2)} KB`);
|
||||
console.log(`平均每实体内存: ${(memoryIncrease / entityCount).toFixed(1)} bytes`);
|
||||
}
|
||||
|
||||
expect(snapshot.entities).toHaveLength(entityCount);
|
||||
|
||||
// 清理
|
||||
entities.length = 0;
|
||||
});
|
||||
});
|
||||
|
||||
describe('极端情况性能', () => {
|
||||
it('应该处理大量小组件的性能', () => {
|
||||
const componentCount = 5000;
|
||||
const components: PerfPositionComponent[] = [];
|
||||
|
||||
// 创建大量小组件
|
||||
for (let i = 0; i < componentCount; i++) {
|
||||
components.push(new PerfPositionComponent(i, i * 2, i * 3));
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
for (const component of components) {
|
||||
protobufSerializer.serialize(component);
|
||||
}
|
||||
const time = performance.now() - start;
|
||||
|
||||
console.log(`\\n=== 大量小组件性能测试 ===`);
|
||||
console.log(`组件数量: ${componentCount}`);
|
||||
console.log(`总时间: ${time.toFixed(2)}ms`);
|
||||
console.log(`平均每组件: ${(time / componentCount).toFixed(4)}ms`);
|
||||
console.log(`每秒处理: ${Math.floor(componentCount / (time / 1000))} 个组件`);
|
||||
|
||||
expect(time).toBeLessThan(10000); // 不超过10秒
|
||||
expect(time / componentCount).toBeLessThan(2); // 每个组件不超过2ms
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,295 +0,0 @@
|
||||
/**
|
||||
* Protobuf装饰器测试
|
||||
*/
|
||||
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoField,
|
||||
ProtoTypes,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtobufRegistry,
|
||||
isProtoSerializable,
|
||||
getProtoName
|
||||
} from '../../src/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('TestPosition')
|
||||
class TestPositionComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public y: number = 0;
|
||||
|
||||
@ProtoFloat(3)
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('TestPlayer')
|
||||
class TestPlayerComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public name: string = '';
|
||||
|
||||
@ProtoInt32(2)
|
||||
public level: number = 1;
|
||||
|
||||
@ProtoInt32(3)
|
||||
public health: number = 100;
|
||||
|
||||
@ProtoBool(4)
|
||||
public isAlive: boolean = true;
|
||||
|
||||
constructor(name: string = '', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
// 没有装饰器的组件
|
||||
class PlainComponent extends Component {
|
||||
public data: string = 'test';
|
||||
}
|
||||
|
||||
// 测试字段编号冲突的组件
|
||||
const createConflictingComponent = () => {
|
||||
try {
|
||||
@ProtoSerializable('Conflict')
|
||||
class ConflictComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(1) // 故意使用相同的字段编号
|
||||
public y: number = 0;
|
||||
}
|
||||
return ConflictComponent;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
describe('ProtobufDecorators', () => {
|
||||
let registry: ProtobufRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
// 获取注册表实例
|
||||
registry = ProtobufRegistry.getInstance();
|
||||
// 不清理状态,因为装饰器在模块加载时已执行
|
||||
});
|
||||
|
||||
describe('@ProtoSerializable装饰器', () => {
|
||||
it('应该正确标记组件为可序列化', () => {
|
||||
const component = new TestPositionComponent(10, 20, 30);
|
||||
|
||||
expect(isProtoSerializable(component)).toBe(true);
|
||||
expect(getProtoName(component)).toBe('TestPosition');
|
||||
});
|
||||
|
||||
it('应该在注册表中注册组件定义', () => {
|
||||
expect(registry.hasProtoDefinition('TestPosition')).toBe(true);
|
||||
expect(registry.hasProtoDefinition('TestPlayer')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确处理没有装饰器的组件', () => {
|
||||
const component = new PlainComponent();
|
||||
|
||||
expect(isProtoSerializable(component)).toBe(false);
|
||||
expect(getProtoName(component)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('@ProtoField装饰器', () => {
|
||||
it('应该正确定义字段', () => {
|
||||
const definition = registry.getComponentDefinition('TestPosition');
|
||||
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition!.fields.size).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const xField = definition!.fields.get('x');
|
||||
expect(xField).toEqual({
|
||||
fieldNumber: 1,
|
||||
type: ProtoTypes.FLOAT,
|
||||
repeated: false,
|
||||
optional: false,
|
||||
name: 'x',
|
||||
customTypeName: undefined,
|
||||
enumValues: undefined,
|
||||
defaultValue: undefined,
|
||||
syncPriority: 'medium',
|
||||
precision: undefined,
|
||||
interpolation: false,
|
||||
quantizationBits: undefined,
|
||||
changeThreshold: 0
|
||||
});
|
||||
|
||||
const yField = definition!.fields.get('y');
|
||||
expect(yField).toEqual({
|
||||
fieldNumber: 2,
|
||||
type: ProtoTypes.FLOAT,
|
||||
repeated: false,
|
||||
optional: false,
|
||||
name: 'y',
|
||||
customTypeName: undefined,
|
||||
enumValues: undefined,
|
||||
defaultValue: undefined,
|
||||
syncPriority: 'medium',
|
||||
precision: undefined,
|
||||
interpolation: false,
|
||||
quantizationBits: undefined,
|
||||
changeThreshold: 0
|
||||
});
|
||||
});
|
||||
|
||||
it('应该支持不同的字段类型', () => {
|
||||
const definition = registry.getComponentDefinition('TestPlayer');
|
||||
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition!.fields.size).toBeGreaterThanOrEqual(4);
|
||||
|
||||
const nameField = definition!.fields.get('name');
|
||||
expect(nameField!.type).toBe(ProtoTypes.STRING);
|
||||
|
||||
const levelField = definition!.fields.get('level');
|
||||
expect(levelField!.type).toBe(ProtoTypes.INT32);
|
||||
|
||||
const healthField = definition!.fields.get('health');
|
||||
expect(healthField!.type).toBe(ProtoTypes.INT32);
|
||||
|
||||
const isAliveField = definition!.fields.get('isAlive');
|
||||
expect(isAliveField!.type).toBe(ProtoTypes.BOOL);
|
||||
});
|
||||
|
||||
it('应该检测字段编号冲突', () => {
|
||||
const result = createConflictingComponent();
|
||||
expect(result).toBeInstanceOf(Error);
|
||||
expect((result as Error).message).toContain('字段编号 1 已被字段');
|
||||
});
|
||||
|
||||
it('应该验证字段编号有效性', () => {
|
||||
expect(() => {
|
||||
class InvalidFieldComponent extends Component {
|
||||
@ProtoField(0) // 无效的字段编号
|
||||
public invalid: number = 0;
|
||||
}
|
||||
}).toThrow('字段编号必须大于0');
|
||||
|
||||
expect(() => {
|
||||
class InvalidFieldComponent extends Component {
|
||||
@ProtoField(-1) // 无效的字段编号
|
||||
public invalid: number = 0;
|
||||
}
|
||||
}).toThrow('字段编号必须大于0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('便捷装饰器', () => {
|
||||
it('ProtoFloat应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('FloatTest')
|
||||
class FloatTestComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public value: number = 0;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('FloatTest');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoTypes.FLOAT);
|
||||
});
|
||||
|
||||
it('ProtoInt32应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('Int32Test')
|
||||
class Int32TestComponent extends Component {
|
||||
@ProtoInt32(1)
|
||||
public value: number = 0;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('Int32Test');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoTypes.INT32);
|
||||
});
|
||||
|
||||
it('ProtoString应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('StringTest')
|
||||
class StringTestComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public value: string = '';
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('StringTest');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoTypes.STRING);
|
||||
});
|
||||
|
||||
it('ProtoBool应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('BoolTest')
|
||||
class BoolTestComponent extends Component {
|
||||
@ProtoBool(1)
|
||||
public value: boolean = false;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('BoolTest');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoTypes.BOOL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProtobufRegistry', () => {
|
||||
it('应该正确生成proto定义', () => {
|
||||
const protoDefinition = registry.generateProtoDefinition();
|
||||
|
||||
expect(protoDefinition).toContain('syntax = "proto3";');
|
||||
expect(protoDefinition).toContain('package ecs;');
|
||||
expect(protoDefinition).toContain('message TestPosition');
|
||||
expect(protoDefinition).toContain('message TestPlayer');
|
||||
expect(protoDefinition).toContain('float x = 1;');
|
||||
expect(protoDefinition).toContain('float y = 2;');
|
||||
expect(protoDefinition).toContain('string name = 1;');
|
||||
expect(protoDefinition).toContain('int32 level = 2;');
|
||||
expect(protoDefinition).toContain('bool isAlive = 4;');
|
||||
});
|
||||
|
||||
it('应该正确管理组件注册', () => {
|
||||
const allComponents = registry.getAllComponents();
|
||||
|
||||
expect(allComponents.size).toBeGreaterThanOrEqual(1);
|
||||
// 由于测试执行顺序不确定,只检查有组件注册即可
|
||||
expect(allComponents.size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('字段选项', () => {
|
||||
it('应该支持repeated字段', () => {
|
||||
@ProtoSerializable('RepeatedTest')
|
||||
class RepeatedTestComponent extends Component {
|
||||
@ProtoField(1, ProtoTypes.INT32, { repeated: true })
|
||||
public values: number[] = [];
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('RepeatedTest');
|
||||
const field = definition!.fields.get('values');
|
||||
expect(field!.repeated).toBe(true);
|
||||
});
|
||||
|
||||
it('应该支持optional字段', () => {
|
||||
@ProtoSerializable('OptionalTest')
|
||||
class OptionalTestComponent extends Component {
|
||||
@ProtoField(1, ProtoTypes.STRING, { optional: true })
|
||||
public optionalValue?: string;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('OptionalTest');
|
||||
const field = definition!.fields.get('optionalValue');
|
||||
expect(field!.optional).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,295 +0,0 @@
|
||||
/**
|
||||
* Protobuf序列化器测试
|
||||
*/
|
||||
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { ProtobufSerializer } from '../../src/Serialization/ProtobufSerializer';
|
||||
import { SerializedData } from '../../src/Serialization/SerializationTypes';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtobufRegistry
|
||||
} from '../../src/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('Position')
|
||||
class PositionComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public y: number = 0;
|
||||
|
||||
@ProtoFloat(3)
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('Health')
|
||||
class HealthComponent extends Component {
|
||||
@ProtoInt32(1)
|
||||
public maxHealth: number = 100;
|
||||
|
||||
@ProtoInt32(2)
|
||||
public currentHealth: number = 100;
|
||||
|
||||
@ProtoBool(3)
|
||||
public isDead: boolean = false;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
|
||||
takeDamage(damage: number): void {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
this.isDead = this.currentHealth <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('Player')
|
||||
class PlayerComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public playerName: string = '';
|
||||
|
||||
@ProtoInt32(2)
|
||||
public playerId: number = 0;
|
||||
|
||||
@ProtoInt32(3)
|
||||
public level: number = 1;
|
||||
|
||||
constructor(playerId: number = 0, playerName: string = '') {
|
||||
super();
|
||||
this.playerId = playerId;
|
||||
this.playerName = playerName;
|
||||
}
|
||||
}
|
||||
|
||||
// 没有protobuf装饰器的组件
|
||||
class CustomComponent extends Component {
|
||||
public customData = {
|
||||
settings: { volume: 0.8 },
|
||||
achievements: ['first_kill', 'level_up'],
|
||||
inventory: new Map([['sword', 1], ['potion', 3]])
|
||||
};
|
||||
|
||||
// 自定义序列化方法
|
||||
serialize(): any {
|
||||
return {
|
||||
customData: {
|
||||
settings: this.customData.settings,
|
||||
achievements: this.customData.achievements,
|
||||
inventory: Array.from(this.customData.inventory.entries())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data: any): void {
|
||||
if (data.customData) {
|
||||
this.customData.settings = data.customData.settings || this.customData.settings;
|
||||
this.customData.achievements = data.customData.achievements || this.customData.achievements;
|
||||
if (data.customData.inventory) {
|
||||
this.customData.inventory = new Map(data.customData.inventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock protobuf.js Root
|
||||
const mockProtobufRoot = {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||
// 模拟protobuf消息类型
|
||||
return {
|
||||
verify: jest.fn().mockReturnValue(null), // 验证通过
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) // 模拟编码结果
|
||||
}),
|
||||
decode: jest.fn().mockImplementation(() => ({
|
||||
x: 10, y: 20, z: 30,
|
||||
maxHealth: 100, currentHealth: 80, isDead: false,
|
||||
playerName: 'TestPlayer', playerId: 1001, level: 5
|
||||
})),
|
||||
toObject: jest.fn().mockImplementation((message) => message),
|
||||
fromObject: jest.fn().mockImplementation((obj) => obj)
|
||||
};
|
||||
})
|
||||
} as any;
|
||||
|
||||
describe('ProtobufSerializer', () => {
|
||||
let serializer: ProtobufSerializer;
|
||||
|
||||
beforeEach(() => {
|
||||
serializer = ProtobufSerializer.getInstance();
|
||||
// 重置mock
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('初始化', () => {
|
||||
it('应该正确初始化protobuf支持', () => {
|
||||
serializer.initialize(mockProtobufRoot);
|
||||
|
||||
expect(serializer.canSerialize(new PositionComponent())).toBe(true);
|
||||
});
|
||||
|
||||
it('自动初始化后应该能够序列化protobuf组件', () => {
|
||||
const newSerializer = new (ProtobufSerializer as any)();
|
||||
expect(newSerializer.canSerialize(new PositionComponent())).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('序列化', () => {
|
||||
beforeEach(() => {
|
||||
serializer.initialize(mockProtobufRoot);
|
||||
});
|
||||
|
||||
it('应该正确序列化protobuf组件', () => {
|
||||
const position = new PositionComponent(10, 20, 30);
|
||||
const result = serializer.serialize(position);
|
||||
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.componentType).toBe('PositionComponent');
|
||||
expect(result.data).toBeInstanceOf(Uint8Array);
|
||||
expect(result.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该正确序列化复杂protobuf组件', () => {
|
||||
const health = new HealthComponent(150);
|
||||
health.takeDamage(50);
|
||||
|
||||
const result = serializer.serialize(health);
|
||||
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.componentType).toBe('HealthComponent');
|
||||
expect(result.data).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('应该拒绝非protobuf组件并抛出错误', () => {
|
||||
const custom = new CustomComponent();
|
||||
|
||||
expect(() => {
|
||||
serializer.serialize(custom);
|
||||
}).toThrow('组件 CustomComponent 不支持protobuf序列化,请添加@ProtoSerializable装饰器');
|
||||
});
|
||||
|
||||
it.skip('protobuf验证失败时应该抛出错误(跳过mock测试)', () => {
|
||||
// 此测试跳过,因为mock验证在重构后需要更复杂的设置
|
||||
});
|
||||
});
|
||||
|
||||
describe('反序列化', () => {
|
||||
beforeEach(() => {
|
||||
serializer.initialize(mockProtobufRoot);
|
||||
});
|
||||
|
||||
it('应该正确反序列化protobuf数据', () => {
|
||||
const position = new PositionComponent();
|
||||
const serializedData: SerializedData = {
|
||||
type: 'protobuf',
|
||||
componentType: 'PositionComponent',
|
||||
data: new Uint8Array([1, 2, 3, 4]),
|
||||
size: 4
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
serializer.deserialize(position, serializedData);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该拒绝非protobuf数据并抛出错误', () => {
|
||||
const custom = new CustomComponent();
|
||||
|
||||
const serializedData: SerializedData = {
|
||||
type: 'json',
|
||||
componentType: 'CustomComponent',
|
||||
data: {},
|
||||
size: 100
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
serializer.deserialize(custom, serializedData);
|
||||
}).toThrow('不支持的序列化类型: json');
|
||||
});
|
||||
|
||||
it('应该处理反序列化错误', () => {
|
||||
const position = new PositionComponent();
|
||||
const invalidData: SerializedData = {
|
||||
type: 'protobuf',
|
||||
componentType: 'PositionComponent',
|
||||
data: new Uint8Array([255, 255, 255, 255]), // 无效数据
|
||||
size: 4
|
||||
};
|
||||
|
||||
// 模拟解码失败
|
||||
const mockType = mockProtobufRoot.lookupType('ecs.Position');
|
||||
mockType.decode.mockImplementation(() => {
|
||||
throw new Error('解码失败');
|
||||
});
|
||||
|
||||
// 应该不抛出异常
|
||||
expect(() => {
|
||||
serializer.deserialize(position, invalidData);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息', () => {
|
||||
it('应该返回正确的统计信息', () => {
|
||||
serializer.initialize(mockProtobufRoot);
|
||||
const stats = serializer.getStats();
|
||||
|
||||
expect(stats.protobufAvailable).toBe(true);
|
||||
expect(stats.registeredComponents).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('自动初始化后应该返回正确的状态', () => {
|
||||
const newSerializer = new (ProtobufSerializer as any)();
|
||||
const stats = newSerializer.getStats();
|
||||
|
||||
expect(stats.protobufAvailable).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
beforeEach(() => {
|
||||
serializer.initialize(mockProtobufRoot);
|
||||
});
|
||||
|
||||
it('应该处理空值和undefined', () => {
|
||||
const position = new PositionComponent();
|
||||
// 设置一些undefined值
|
||||
(position as any).undefinedProp = undefined;
|
||||
(position as any).nullProp = null;
|
||||
|
||||
const result = serializer.serialize(position);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该拒绝非protobuf组件', () => {
|
||||
const custom = new CustomComponent();
|
||||
// 创建循环引用
|
||||
(custom as any).circular = custom;
|
||||
|
||||
expect(() => {
|
||||
serializer.serialize(custom);
|
||||
}).toThrow('组件 CustomComponent 不支持protobuf序列化');
|
||||
});
|
||||
|
||||
it('应该处理非常大的数值', () => {
|
||||
const position = new PositionComponent(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, 0);
|
||||
|
||||
const result = serializer.serialize(position);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,438 +0,0 @@
|
||||
/**
|
||||
* Protobuf序列化器边界情况测试
|
||||
*/
|
||||
|
||||
import { Component, BigIntFactory } from '@esengine/ecs-framework';
|
||||
import { ProtobufSerializer } from '../../src/Serialization/ProtobufSerializer';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtoBytes,
|
||||
ProtoTimestamp,
|
||||
ProtoDouble,
|
||||
ProtoInt64,
|
||||
ProtoStruct
|
||||
} from '../../src/Serialization/ProtobufDecorators';
|
||||
|
||||
// 边界测试组件
|
||||
@ProtoSerializable('EdgeCaseComponent')
|
||||
class EdgeCaseComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public floatValue: number = 0;
|
||||
|
||||
@ProtoDouble(2)
|
||||
public doubleValue: number = 0;
|
||||
|
||||
@ProtoInt32(3)
|
||||
public intValue: number = 0;
|
||||
|
||||
@ProtoInt64(4)
|
||||
public bigIntValue: any = BigIntFactory.zero();
|
||||
|
||||
@ProtoString(5)
|
||||
public stringValue: string = '';
|
||||
|
||||
@ProtoBool(6)
|
||||
public boolValue: boolean = false;
|
||||
|
||||
@ProtoBytes(7)
|
||||
public bytesValue: Uint8Array = new Uint8Array();
|
||||
|
||||
@ProtoTimestamp(8)
|
||||
public timestampValue: Date = new Date();
|
||||
|
||||
@ProtoStruct(9)
|
||||
public structValue: any = {};
|
||||
|
||||
@ProtoFloat(10, { repeated: true })
|
||||
public arrayValue: number[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// 不完整的组件(缺少字段)
|
||||
@ProtoSerializable('IncompleteComponent')
|
||||
class IncompleteComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public name: string = '';
|
||||
|
||||
// 故意添加没有装饰器的字段
|
||||
public undecoratedField: number = 42;
|
||||
|
||||
constructor(name: string = '') {
|
||||
super();
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// 有循环引用的组件
|
||||
@ProtoSerializable('CircularComponent')
|
||||
class CircularComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public name: string = '';
|
||||
|
||||
@ProtoStruct(2)
|
||||
public circular: any = null;
|
||||
|
||||
constructor(name: string = '') {
|
||||
super();
|
||||
this.name = name;
|
||||
// 创建循环引用
|
||||
this.circular = this;
|
||||
}
|
||||
}
|
||||
|
||||
// 没有protobuf装饰器的组件
|
||||
class NonSerializableComponent extends Component {
|
||||
public data: string = 'test';
|
||||
|
||||
serialize(): any {
|
||||
return { data: this.data };
|
||||
}
|
||||
|
||||
deserialize(data: any): void {
|
||||
this.data = data.data || this.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Mock protobuf.js
|
||||
const mockProtobuf = {
|
||||
Root: jest.fn(),
|
||||
Type: jest.fn(),
|
||||
Field: jest.fn(),
|
||||
parse: jest.fn().mockReturnValue({
|
||||
root: {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||
return {
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||
}),
|
||||
decode: jest.fn().mockImplementation(() => ({
|
||||
floatValue: 3.14,
|
||||
doubleValue: 2.718,
|
||||
intValue: 42,
|
||||
bigIntValue: BigIntFactory.create(999),
|
||||
stringValue: 'test',
|
||||
boolValue: true,
|
||||
bytesValue: new Uint8Array([65, 66, 67]),
|
||||
timestampValue: { seconds: 1609459200, nanos: 0 },
|
||||
structValue: { fields: { key: { stringValue: 'value' } } },
|
||||
arrayValue: [1.1, 2.2, 3.3],
|
||||
name: 'TestComponent'
|
||||
})),
|
||||
toObject: jest.fn().mockImplementation((message) => message),
|
||||
fromObject: jest.fn().mockImplementation((obj) => obj)
|
||||
};
|
||||
}),
|
||||
lookupTypeOrEnum: jest.fn().mockImplementation((typeName: string) => {
|
||||
if (typeName === 'google.protobuf.Timestamp') {
|
||||
return {
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||
}),
|
||||
decode: jest.fn().mockImplementation(() => ({
|
||||
seconds: 1609459200,
|
||||
nanos: 0
|
||||
})),
|
||||
toObject: jest.fn().mockImplementation((message) => message),
|
||||
fromObject: jest.fn().mockImplementation((obj) => obj)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
describe('ProtobufSerializer边界情况测试', () => {
|
||||
let serializer: ProtobufSerializer;
|
||||
|
||||
beforeEach(() => {
|
||||
serializer = ProtobufSerializer.getInstance();
|
||||
serializer.initialize(mockProtobuf.parse().root as any);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('极值测试', () => {
|
||||
it('应该处理极大值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.floatValue = Number.MAX_VALUE;
|
||||
component.doubleValue = Number.MAX_VALUE;
|
||||
component.intValue = Number.MAX_SAFE_INTEGER;
|
||||
component.bigIntValue = BigIntFactory.create(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该处理极小值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.floatValue = Number.MIN_VALUE;
|
||||
component.doubleValue = Number.MIN_VALUE;
|
||||
component.intValue = Number.MIN_SAFE_INTEGER;
|
||||
component.bigIntValue = BigIntFactory.create(Number.MIN_SAFE_INTEGER);
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理特殊数值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.floatValue = NaN;
|
||||
component.doubleValue = Infinity;
|
||||
component.intValue = 0;
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('空值和undefined测试', () => {
|
||||
it('应该处理null值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
(component as any).stringValue = null;
|
||||
(component as any).structValue = null;
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理undefined值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
(component as any).stringValue = undefined;
|
||||
(component as any).floatValue = undefined;
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理空数组', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.arrayValue = [];
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('复杂数据类型测试', () => {
|
||||
it('应该处理复杂对象结构', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.structValue = {
|
||||
nested: {
|
||||
array: [1, 2, 3],
|
||||
object: { key: 'value' },
|
||||
date: new Date(),
|
||||
null: null,
|
||||
undefined: undefined
|
||||
}
|
||||
};
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理Date对象', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.timestampValue = new Date('2021-01-01T00:00:00Z');
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理Uint8Array', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.bytesValue = new Uint8Array([0, 255, 128, 64]);
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('循环引用测试', () => {
|
||||
it('应该处理循环引用对象', () => {
|
||||
const component = new CircularComponent('circular');
|
||||
|
||||
// 循环引用应该被妥善处理
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.componentType).toBe('CircularComponent');
|
||||
});
|
||||
});
|
||||
|
||||
describe('不完整组件测试', () => {
|
||||
it('应该处理缺少装饰器的字段', () => {
|
||||
const component = new IncompleteComponent('test');
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.componentType).toBe('IncompleteComponent');
|
||||
});
|
||||
});
|
||||
|
||||
describe('非序列化组件测试', () => {
|
||||
it('应该拒绝非protobuf组件并抛出错误', () => {
|
||||
const component = new NonSerializableComponent();
|
||||
|
||||
// 没有protobuf装饰器的组件应该抛出错误,不再回退到JSON序列化
|
||||
expect(() => {
|
||||
serializer.serialize(component);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量序列化边界测试', () => {
|
||||
it('应该处理空数组', () => {
|
||||
const results = serializer.serializeBatch([]);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该处理混合组件类型', () => {
|
||||
const components = [
|
||||
new EdgeCaseComponent(),
|
||||
new NonSerializableComponent(),
|
||||
new IncompleteComponent('mixed'),
|
||||
];
|
||||
|
||||
// continueOnError: true 时,只有可序列化的组件能成功,其他会被跳过
|
||||
const results = serializer.serializeBatch(components, { continueOnError: true });
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
expect(results.every(r => r.type === 'protobuf')).toBe(true);
|
||||
|
||||
// continueOnError: false 时应该抛出错误
|
||||
expect(() => {
|
||||
serializer.serializeBatch(components, { continueOnError: false });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理批量数据', () => {
|
||||
const components = Array.from({ length: 50 }, () => new EdgeCaseComponent());
|
||||
|
||||
const results = serializer.serializeBatch(components);
|
||||
expect(results).toHaveLength(50);
|
||||
expect(results.every(r => r.type === 'protobuf')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理序列化错误', () => {
|
||||
// 创建会导致序列化失败的组件
|
||||
const components = [new NonSerializableComponent()];
|
||||
|
||||
// continueOnError = false 应该抛出异常
|
||||
expect(() => {
|
||||
serializer.serializeBatch(components, { continueOnError: false });
|
||||
}).toThrow();
|
||||
|
||||
// continueOnError = true 应该返回空数组(跳过失败的组件)
|
||||
const results = serializer.serializeBatch(components, { continueOnError: true });
|
||||
expect(results).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('反序列化边界测试', () => {
|
||||
it('应该拒绝JSON类型的反序列化并抛出错误', () => {
|
||||
const component = new NonSerializableComponent();
|
||||
const serializedData = {
|
||||
type: 'json' as const,
|
||||
componentType: 'NonSerializableComponent',
|
||||
data: { data: 'deserialized' },
|
||||
size: 100
|
||||
};
|
||||
|
||||
// JSON类型的数据应该被拒绝,抛出错误
|
||||
expect(() => {
|
||||
serializer.deserialize(component, serializedData);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该优雅处理反序列化错误', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
const invalidData = {
|
||||
type: 'protobuf' as const,
|
||||
componentType: 'EdgeCaseComponent',
|
||||
data: new Uint8Array([255, 255, 255, 255]),
|
||||
size: 4
|
||||
};
|
||||
|
||||
// 模拟解码失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType('ecs.EdgeCaseComponent');
|
||||
mockType.decode.mockImplementation(() => {
|
||||
throw new Error('Decode failed');
|
||||
});
|
||||
|
||||
// 不应该抛出异常
|
||||
expect(() => {
|
||||
serializer.deserialize(component, invalidData);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理缺失的proto定义', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
// 清除proto名称以模拟缺失情况
|
||||
(component as any)._protoName = undefined;
|
||||
|
||||
const serializedData = {
|
||||
type: 'protobuf' as const,
|
||||
componentType: 'EdgeCaseComponent',
|
||||
data: new Uint8Array([1, 2, 3, 4]),
|
||||
size: 4
|
||||
};
|
||||
|
||||
// 应该抛出未设置protobuf名称的错误
|
||||
expect(() => {
|
||||
serializer.deserialize(component, serializedData);
|
||||
}).toThrow('组件 EdgeCaseComponent 未设置protobuf名称');
|
||||
});
|
||||
});
|
||||
|
||||
describe('缓存测试', () => {
|
||||
it('应该能清空所有缓存', () => {
|
||||
serializer.clearAllCaches();
|
||||
const stats = serializer.getStats();
|
||||
expect(stats.messageTypeCacheSize).toBe(0);
|
||||
expect(stats.componentDataCacheSize).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能选项测试', () => {
|
||||
it('应该能禁用数据验证', () => {
|
||||
serializer.setPerformanceOptions({ enableValidation: false });
|
||||
|
||||
const component = new EdgeCaseComponent();
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该能禁用组件数据缓存', () => {
|
||||
serializer.setPerformanceOptions({ enableComponentDataCache: false });
|
||||
|
||||
const component = new EdgeCaseComponent();
|
||||
serializer.serialize(component);
|
||||
|
||||
const stats = serializer.getStats();
|
||||
expect(stats.componentDataCacheSize).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息测试', () => {
|
||||
it('应该返回正确的统计信息', () => {
|
||||
const stats = serializer.getStats();
|
||||
|
||||
expect(typeof stats.registeredComponents).toBe('number');
|
||||
expect(typeof stats.protobufAvailable).toBe('boolean');
|
||||
expect(typeof stats.messageTypeCacheSize).toBe('number');
|
||||
expect(typeof stats.componentDataCacheSize).toBe('number');
|
||||
expect(typeof stats.enableComponentDataCache).toBe('boolean');
|
||||
expect(typeof stats.maxCacheSize).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,393 +0,0 @@
|
||||
/**
|
||||
* 真实 Protobuf 序列化性能测试
|
||||
* 使用实际的 protobufjs 库进行性能对比
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtobufRegistry
|
||||
} from '../../src/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('Position')
|
||||
class PositionComponent extends Component {
|
||||
@ProtoFloat(1) public x: number = 0;
|
||||
@ProtoFloat(2) public y: number = 0;
|
||||
@ProtoFloat(3) public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('Player')
|
||||
class PlayerComponent extends Component {
|
||||
@ProtoString(1) public name: string = '';
|
||||
@ProtoInt32(2) public level: number = 1;
|
||||
@ProtoInt32(3) public experience: number = 0;
|
||||
@ProtoInt32(4) public score: number = 0;
|
||||
@ProtoBool(5) public isOnline: boolean = true;
|
||||
@ProtoFloat(6) public health: number = 100.0;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
this.experience = level * 1000;
|
||||
this.score = level * 500;
|
||||
this.health = 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
// JSON 对比组件
|
||||
class JsonPositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
class JsonPlayerComponent extends Component {
|
||||
public name: string = '';
|
||||
public level: number = 1;
|
||||
public experience: number = 0;
|
||||
public score: number = 0;
|
||||
public isOnline: boolean = true;
|
||||
public health: number = 100.0;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
this.experience = level * 1000;
|
||||
this.score = level * 500;
|
||||
this.health = 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
describe('真实 Protobuf 性能测试', () => {
|
||||
let protobuf: any;
|
||||
let root: any;
|
||||
let PositionType: any;
|
||||
let PlayerType: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
// 尝试加载真实的 protobufjs
|
||||
protobuf = require('protobufjs');
|
||||
|
||||
// 生成 proto 定义
|
||||
const registry = ProtobufRegistry.getInstance();
|
||||
const protoDefinition = registry.generateProtoDefinition();
|
||||
|
||||
console.log('Generated proto definition:');
|
||||
console.log(protoDefinition);
|
||||
|
||||
// 解析 proto 定义
|
||||
root = protobuf.parse(protoDefinition).root;
|
||||
PositionType = root.lookupType('ecs.Position');
|
||||
PlayerType = root.lookupType('ecs.Player');
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Protobuf not available, skipping real performance tests:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const skipIfNoProtobuf = () => {
|
||||
if (!protobuf || !root) {
|
||||
console.log('Skipping test: protobufjs not available');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
describe('简单组件性能对比', () => {
|
||||
it('Position 组件序列化性能', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
const iterations = 1000;
|
||||
const protobufComponents: PositionComponent[] = [];
|
||||
const jsonComponents: JsonPositionComponent[] = [];
|
||||
|
||||
// 准备测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const x = Math.random() * 1000;
|
||||
const y = Math.random() * 1000;
|
||||
const z = Math.random() * 100;
|
||||
|
||||
protobufComponents.push(new PositionComponent(x, y, z));
|
||||
jsonComponents.push(new JsonPositionComponent(x, y, z));
|
||||
}
|
||||
|
||||
// Protobuf 序列化测试
|
||||
const protobufStartTime = performance.now();
|
||||
let protobufTotalSize = 0;
|
||||
const protobufResults: Uint8Array[] = [];
|
||||
|
||||
for (const component of protobufComponents) {
|
||||
const message = PositionType.create({
|
||||
x: component.x,
|
||||
y: component.y,
|
||||
z: component.z
|
||||
});
|
||||
const buffer = PositionType.encode(message).finish();
|
||||
protobufResults.push(buffer);
|
||||
protobufTotalSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufEndTime = performance.now();
|
||||
const protobufTime = protobufEndTime - protobufStartTime;
|
||||
|
||||
// JSON 序列化测试
|
||||
const jsonStartTime = performance.now();
|
||||
let jsonTotalSize = 0;
|
||||
const jsonResults: string[] = [];
|
||||
|
||||
for (const component of jsonComponents) {
|
||||
const jsonString = JSON.stringify({
|
||||
x: component.x,
|
||||
y: component.y,
|
||||
z: component.z
|
||||
});
|
||||
jsonResults.push(jsonString);
|
||||
jsonTotalSize += new Blob([jsonString]).size;
|
||||
}
|
||||
|
||||
const jsonEndTime = performance.now();
|
||||
const jsonTime = jsonEndTime - jsonStartTime;
|
||||
|
||||
// 计算性能指标
|
||||
const speedImprovement = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0;
|
||||
const sizeReduction = jsonTotalSize > 0 ? ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100) : 0;
|
||||
|
||||
console.log(`\\n=== Position 组件性能对比 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`);
|
||||
console.log(`速度变化: ${speedImprovement > 0 ? '+' : ''}${speedImprovement.toFixed(1)}%`);
|
||||
console.log('');
|
||||
console.log(`Protobuf 总大小: ${protobufTotalSize} bytes`);
|
||||
console.log(`JSON 总大小: ${jsonTotalSize} bytes`);
|
||||
console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`);
|
||||
console.log(`平均 Protobuf 大小: ${(protobufTotalSize / iterations).toFixed(1)} bytes`);
|
||||
console.log(`平均 JSON 大小: ${(jsonTotalSize / iterations).toFixed(1)} bytes`);
|
||||
|
||||
// 验证反序列化
|
||||
let deserializeTime = performance.now();
|
||||
for (const buffer of protobufResults.slice(0, 10)) { // 只测试前10个
|
||||
const decoded = PositionType.decode(buffer);
|
||||
expect(typeof decoded.x).toBe('number');
|
||||
expect(typeof decoded.y).toBe('number');
|
||||
expect(typeof decoded.z).toBe('number');
|
||||
}
|
||||
deserializeTime = performance.now() - deserializeTime;
|
||||
console.log(`Protobuf 反序列化 10 个: ${deserializeTime.toFixed(2)}ms`);
|
||||
|
||||
// 基本验证
|
||||
expect(protobufTime).toBeGreaterThan(0);
|
||||
expect(jsonTime).toBeGreaterThan(0);
|
||||
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('复杂 Player 组件序列化性能', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
const iterations = 500;
|
||||
const protobufPlayers: PlayerComponent[] = [];
|
||||
const jsonPlayers: JsonPlayerComponent[] = [];
|
||||
|
||||
// 创建测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const name = `Player_${i}_${'x'.repeat(10 + Math.floor(Math.random() * 20))}`;
|
||||
const level = Math.floor(Math.random() * 100) + 1;
|
||||
|
||||
protobufPlayers.push(new PlayerComponent(name, level));
|
||||
jsonPlayers.push(new JsonPlayerComponent(name, level));
|
||||
}
|
||||
|
||||
// Protobuf 序列化测试
|
||||
const protobufStart = performance.now();
|
||||
let protobufSize = 0;
|
||||
|
||||
for (const player of protobufPlayers) {
|
||||
const message = PlayerType.create({
|
||||
name: player.name,
|
||||
level: player.level,
|
||||
experience: player.experience,
|
||||
score: player.score,
|
||||
isOnline: player.isOnline,
|
||||
health: player.health
|
||||
});
|
||||
const buffer = PlayerType.encode(message).finish();
|
||||
protobufSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON 序列化测试
|
||||
const jsonStart = performance.now();
|
||||
let jsonSize = 0;
|
||||
|
||||
for (const player of jsonPlayers) {
|
||||
const jsonString = JSON.stringify({
|
||||
name: player.name,
|
||||
level: player.level,
|
||||
experience: player.experience,
|
||||
score: player.score,
|
||||
isOnline: player.isOnline,
|
||||
health: player.health
|
||||
});
|
||||
jsonSize += new Blob([jsonString]).size;
|
||||
}
|
||||
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
const speedChange = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0;
|
||||
const sizeReduction = jsonSize > 0 ? ((jsonSize - protobufSize) / jsonSize * 100) : 0;
|
||||
|
||||
console.log(`\\n=== Player 组件性能对比 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`);
|
||||
console.log(`速度变化: ${speedChange > 0 ? '+' : ''}${speedChange.toFixed(1)}%`);
|
||||
console.log('');
|
||||
console.log(`Protobuf 总大小: ${protobufSize} bytes`);
|
||||
console.log(`JSON 总大小: ${jsonSize} bytes`);
|
||||
console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`);
|
||||
console.log(`平均 Protobuf 大小: ${(protobufSize / iterations).toFixed(1)} bytes`);
|
||||
console.log(`平均 JSON 大小: ${(jsonSize / iterations).toFixed(1)} bytes`);
|
||||
|
||||
expect(protobufTime).toBeGreaterThan(0);
|
||||
expect(jsonTime).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量数据性能测试', () => {
|
||||
it('大量小对象序列化', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
const count = 5000;
|
||||
console.log(`\\n=== 大量小对象测试 (${count} 个 Position) ===`);
|
||||
|
||||
// 准备数据
|
||||
const positions = Array.from({ length: count }, (_, i) => ({
|
||||
x: i * 0.1,
|
||||
y: i * 0.2,
|
||||
z: i * 0.05
|
||||
}));
|
||||
|
||||
// Protobuf 批量序列化
|
||||
const protobufStart = performance.now();
|
||||
let protobufSize = 0;
|
||||
|
||||
for (const pos of positions) {
|
||||
const message = PositionType.create(pos);
|
||||
const buffer = PositionType.encode(message).finish();
|
||||
protobufSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON 批量序列化
|
||||
const jsonStart = performance.now();
|
||||
let jsonSize = 0;
|
||||
|
||||
for (const pos of positions) {
|
||||
const jsonString = JSON.stringify(pos);
|
||||
jsonSize += jsonString.length;
|
||||
}
|
||||
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufSize} bytes`);
|
||||
console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonSize} bytes`);
|
||||
console.log(`速度: ${protobufTime < jsonTime ? 'Protobuf 更快' : 'JSON 更快'} (${Math.abs(protobufTime - jsonTime).toFixed(2)}ms 差异)`);
|
||||
console.log(`大小: Protobuf ${protobufSize < jsonSize ? '更小' : '更大'} (${Math.abs(protobufSize - jsonSize)} bytes 差异)`);
|
||||
console.log(`处理速度: Protobuf ${Math.floor(count / (protobufTime / 1000))} ops/s, JSON ${Math.floor(count / (jsonTime / 1000))} ops/s`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('真实网络场景模拟', () => {
|
||||
it('游戏状态同步场景', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
console.log(`\\n=== 游戏状态同步场景 ===`);
|
||||
|
||||
// 模拟 100 个玩家的位置更新
|
||||
const playerCount = 100;
|
||||
const updateData = Array.from({ length: playerCount }, (_, i) => ({
|
||||
playerId: i,
|
||||
x: Math.random() * 1000,
|
||||
y: Math.random() * 1000,
|
||||
z: Math.random() * 100,
|
||||
health: Math.floor(Math.random() * 100),
|
||||
isMoving: Math.random() > 0.5
|
||||
}));
|
||||
|
||||
// 创建组合消息类型(模拟)
|
||||
const GameUpdateType = root.lookupType('ecs.Position'); // 简化使用 Position
|
||||
|
||||
// Protobuf 序列化所有更新
|
||||
const protobufStart = performance.now();
|
||||
let protobufTotalSize = 0;
|
||||
|
||||
for (const update of updateData) {
|
||||
const message = GameUpdateType.create({
|
||||
x: update.x,
|
||||
y: update.y,
|
||||
z: update.z
|
||||
});
|
||||
const buffer = GameUpdateType.encode(message).finish();
|
||||
protobufTotalSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON 序列化所有更新
|
||||
const jsonStart = performance.now();
|
||||
let jsonTotalSize = 0;
|
||||
|
||||
for (const update of updateData) {
|
||||
const jsonString = JSON.stringify({
|
||||
playerId: update.playerId,
|
||||
x: update.x,
|
||||
y: update.y,
|
||||
z: update.z,
|
||||
health: update.health,
|
||||
isMoving: update.isMoving
|
||||
});
|
||||
jsonTotalSize += jsonString.length;
|
||||
}
|
||||
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
console.log(`${playerCount} 个玩家位置更新:`);
|
||||
console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufTotalSize} bytes`);
|
||||
console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonTotalSize} bytes`);
|
||||
|
||||
// 计算网络传输节省
|
||||
const sizeSaving = jsonTotalSize - protobufTotalSize;
|
||||
const percentSaving = (sizeSaving / jsonTotalSize * 100);
|
||||
|
||||
console.log(`数据大小节省: ${sizeSaving} bytes (${percentSaving.toFixed(1)}%)`);
|
||||
console.log(`每秒 60 次更新的带宽节省: ${(sizeSaving * 60 / 1024).toFixed(2)} KB/s`);
|
||||
|
||||
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,370 +0,0 @@
|
||||
/**
|
||||
* SnapshotManager与Protobuf序列化集成测试
|
||||
*/
|
||||
|
||||
import { Entity, Scene, Component } from '@esengine/ecs-framework';
|
||||
import { SnapshotManager } from '../../src/Snapshot/SnapshotManager';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool
|
||||
} from '../../src/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('TestPosition')
|
||||
class TestPositionComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('TestVelocity')
|
||||
class TestVelocityComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public vx: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public vy: number = 0;
|
||||
|
||||
constructor(vx: number = 0, vy: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('TestHealth')
|
||||
class TestHealthComponent extends Component {
|
||||
@ProtoInt32(1)
|
||||
public maxHealth: number = 100;
|
||||
|
||||
@ProtoInt32(2)
|
||||
public currentHealth: number = 100;
|
||||
|
||||
@ProtoBool(3)
|
||||
public isDead: boolean = false;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
}
|
||||
|
||||
// 传统JSON序列化组件
|
||||
class TraditionalComponent extends Component {
|
||||
public customData = {
|
||||
name: 'traditional',
|
||||
values: [1, 2, 3],
|
||||
settings: { enabled: true }
|
||||
};
|
||||
|
||||
serialize(): any {
|
||||
return {
|
||||
customData: this.customData
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data: any): void {
|
||||
if (data.customData) {
|
||||
this.customData = data.customData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 简单组件(使用默认序列化)
|
||||
class SimpleComponent extends Component {
|
||||
public value: number = 42;
|
||||
public text: string = 'simple';
|
||||
public flag: boolean = true;
|
||||
}
|
||||
|
||||
// Mock protobuf.js
|
||||
const mockProtobuf = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
root: {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||
const mockData: Record<string, any> = {
|
||||
'ecs.TestPosition': { x: 10, y: 20 },
|
||||
'ecs.TestVelocity': { vx: 5, vy: 3 },
|
||||
'ecs.TestHealth': { maxHealth: 100, currentHealth: 80, isDead: false }
|
||||
};
|
||||
|
||||
return {
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn().mockImplementation((data: any) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||
}),
|
||||
decode: jest.fn().mockReturnValue(mockData[typeName] || {}),
|
||||
toObject: jest.fn().mockImplementation((message: any) => message)
|
||||
};
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
describe('SnapshotManager Protobuf集成', () => {
|
||||
let snapshotManager: SnapshotManager;
|
||||
let scene: Scene;
|
||||
|
||||
beforeEach(() => {
|
||||
snapshotManager = new SnapshotManager();
|
||||
snapshotManager.initializeProtobuf(mockProtobuf.parse().root as any);
|
||||
scene = new Scene();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('混合序列化快照', () => {
|
||||
it('应该正确创建包含protobuf和JSON组件的快照', () => {
|
||||
// 创建实体
|
||||
const player = scene.createEntity('Player');
|
||||
player.addComponent(new TestPositionComponent(100, 200));
|
||||
player.addComponent(new TestVelocityComponent(5, 3));
|
||||
player.addComponent(new TestHealthComponent(120));
|
||||
player.addComponent(new TraditionalComponent());
|
||||
player.addComponent(new SimpleComponent());
|
||||
|
||||
// 创建快照
|
||||
const snapshot = snapshotManager.createSceneSnapshot([player]);
|
||||
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot.entities).toHaveLength(1);
|
||||
expect(snapshot.entities[0].components).toHaveLength(5);
|
||||
|
||||
// 验证快照包含所有组件
|
||||
const componentTypes = snapshot.entities[0].components.map(c => c.type);
|
||||
expect(componentTypes).toContain('TestPositionComponent');
|
||||
expect(componentTypes).toContain('TestVelocityComponent');
|
||||
expect(componentTypes).toContain('TestHealthComponent');
|
||||
expect(componentTypes).toContain('TraditionalComponent');
|
||||
expect(componentTypes).toContain('SimpleComponent');
|
||||
});
|
||||
|
||||
it('应该根据组件类型使用相应的序列化方式', () => {
|
||||
const entity = scene.createEntity('TestEntity');
|
||||
const position = new TestPositionComponent(50, 75);
|
||||
const traditional = new TraditionalComponent();
|
||||
|
||||
entity.addComponent(position);
|
||||
entity.addComponent(traditional);
|
||||
|
||||
const snapshot = snapshotManager.createSceneSnapshot([entity]);
|
||||
const components = snapshot.entities[0].components;
|
||||
|
||||
// 检查序列化数据格式
|
||||
const positionSnapshot = components.find(c => c.type === 'TestPositionComponent');
|
||||
const traditionalSnapshot = components.find(c => c.type === 'TraditionalComponent');
|
||||
|
||||
expect(positionSnapshot).toBeDefined();
|
||||
expect(traditionalSnapshot).toBeDefined();
|
||||
|
||||
// Protobuf组件应该有SerializedData格式
|
||||
expect(positionSnapshot!.data).toHaveProperty('type');
|
||||
expect(positionSnapshot!.data).toHaveProperty('componentType');
|
||||
expect(positionSnapshot!.data).toHaveProperty('data');
|
||||
expect(positionSnapshot!.data).toHaveProperty('size');
|
||||
});
|
||||
});
|
||||
|
||||
describe('快照恢复', () => {
|
||||
it('应该正确恢复protobuf序列化的组件', () => {
|
||||
// 创建原始实体
|
||||
const originalEntity = scene.createEntity('Original');
|
||||
const originalPosition = new TestPositionComponent(100, 200);
|
||||
const originalHealth = new TestHealthComponent(150);
|
||||
originalHealth.currentHealth = 120;
|
||||
|
||||
originalEntity.addComponent(originalPosition);
|
||||
originalEntity.addComponent(originalHealth);
|
||||
|
||||
// 创建快照
|
||||
const snapshot = snapshotManager.createSceneSnapshot([originalEntity]);
|
||||
|
||||
// 创建新实体进行恢复
|
||||
const newEntity = scene.createEntity('New');
|
||||
newEntity.addComponent(new TestPositionComponent());
|
||||
newEntity.addComponent(new TestHealthComponent());
|
||||
|
||||
// 恢复快照
|
||||
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||
|
||||
// 验证数据被正确恢复(注意:由于使用mock,实际值来自mock数据)
|
||||
const restoredPosition = newEntity.getComponent(TestPositionComponent as any);
|
||||
const restoredHealth = newEntity.getComponent(TestHealthComponent as any);
|
||||
|
||||
expect(restoredPosition).toBeDefined();
|
||||
expect(restoredHealth).toBeDefined();
|
||||
|
||||
// 验证快照恢复成功(有组件数据被恢复)
|
||||
expect((restoredPosition as TestPositionComponent)?.x).toBeDefined();
|
||||
expect((restoredHealth as TestHealthComponent)?.maxHealth).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该正确恢复传统JSON序列化的组件', () => {
|
||||
const originalEntity = scene.createEntity('Original');
|
||||
const originalTraditional = new TraditionalComponent();
|
||||
originalTraditional.customData.name = 'modified';
|
||||
originalTraditional.customData.values = [4, 5, 6];
|
||||
|
||||
originalEntity.addComponent(originalTraditional);
|
||||
|
||||
const snapshot = snapshotManager.createSceneSnapshot([originalEntity]);
|
||||
|
||||
const newEntity = scene.createEntity('New');
|
||||
const newTraditional = new TraditionalComponent();
|
||||
newEntity.addComponent(newTraditional);
|
||||
|
||||
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||
|
||||
// 验证JSON数据被正确恢复(由于使用mock,验证组件被恢复即可)
|
||||
expect(newTraditional.customData).toBeDefined();
|
||||
expect(newTraditional.customData.name).toBe('traditional');
|
||||
expect(newTraditional.customData.values).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('应该处理混合序列化的实体恢复', () => {
|
||||
const originalEntity = scene.createEntity('Mixed');
|
||||
const position = new TestPositionComponent(30, 40);
|
||||
const traditional = new TraditionalComponent();
|
||||
const simple = new SimpleComponent();
|
||||
|
||||
traditional.customData.name = 'mixed_test';
|
||||
simple.value = 99;
|
||||
simple.text = 'updated';
|
||||
|
||||
originalEntity.addComponent(position);
|
||||
originalEntity.addComponent(traditional);
|
||||
originalEntity.addComponent(simple);
|
||||
|
||||
const snapshot = snapshotManager.createSceneSnapshot([originalEntity]);
|
||||
|
||||
const newEntity = scene.createEntity('NewMixed');
|
||||
newEntity.addComponent(new TestPositionComponent());
|
||||
newEntity.addComponent(new TraditionalComponent());
|
||||
newEntity.addComponent(new SimpleComponent());
|
||||
|
||||
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||
|
||||
// 验证所有组件都被正确恢复
|
||||
const restoredTraditional = newEntity.getComponent(TraditionalComponent);
|
||||
const restoredSimple = newEntity.getComponent(SimpleComponent);
|
||||
|
||||
expect(restoredTraditional!.customData.name).toBe('traditional');
|
||||
expect(restoredSimple!.value).toBe(42);
|
||||
expect(restoredSimple!.text).toBe('simple');
|
||||
});
|
||||
});
|
||||
|
||||
describe('向后兼容性', () => {
|
||||
it('应该能够处理旧格式的快照数据', () => {
|
||||
// 模拟旧格式的快照数据
|
||||
const legacySnapshot = {
|
||||
entities: [{
|
||||
id: 1,
|
||||
name: 'LegacyEntity',
|
||||
enabled: true,
|
||||
active: true,
|
||||
tag: 0,
|
||||
updateOrder: 0,
|
||||
components: [{
|
||||
type: 'SimpleComponent',
|
||||
id: 1,
|
||||
data: { value: 123, text: 'legacy', flag: false }, // 直接的JSON数据
|
||||
enabled: true,
|
||||
config: { includeInSnapshot: true, compressionLevel: 0, syncPriority: 5, enableIncremental: true }
|
||||
}],
|
||||
children: [],
|
||||
timestamp: Date.now()
|
||||
}],
|
||||
timestamp: Date.now(),
|
||||
version: '1.0.0',
|
||||
type: 'full' as const
|
||||
};
|
||||
|
||||
const entity = scene.createEntity('TestEntity');
|
||||
entity.addComponent(new SimpleComponent());
|
||||
|
||||
snapshotManager.restoreFromSnapshot(legacySnapshot, [entity]);
|
||||
|
||||
const component = entity.getComponent(SimpleComponent);
|
||||
expect(component!.value).toBe(42);
|
||||
expect(component!.text).toBe('simple');
|
||||
expect(component!.flag).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该优雅地处理protobuf序列化失败', () => {
|
||||
// 模拟protobuf验证失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType;
|
||||
mockType.mockImplementation(() => ({
|
||||
verify: jest.fn().mockReturnValue('验证失败'),
|
||||
create: jest.fn(),
|
||||
encode: jest.fn(),
|
||||
decode: jest.fn(),
|
||||
toObject: jest.fn()
|
||||
}));
|
||||
|
||||
const entity = scene.createEntity('ErrorTest');
|
||||
entity.addComponent(new TestPositionComponent(10, 20));
|
||||
|
||||
// 应该不抛出异常,而是回退到JSON序列化
|
||||
expect(() => {
|
||||
snapshotManager.createSceneSnapshot([entity]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该优雅地处理protobuf反序列化失败', () => {
|
||||
const entity = scene.createEntity('Test');
|
||||
const position = new TestPositionComponent(10, 20);
|
||||
entity.addComponent(position);
|
||||
|
||||
const snapshot = snapshotManager.createSceneSnapshot([entity]);
|
||||
|
||||
// 模拟反序列化失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType;
|
||||
mockType.mockImplementation(() => ({
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn(),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||
}),
|
||||
decode: jest.fn().mockImplementation(() => {
|
||||
throw new Error('解码失败');
|
||||
}),
|
||||
toObject: jest.fn()
|
||||
}));
|
||||
|
||||
const newEntity = scene.createEntity('NewTest');
|
||||
newEntity.addComponent(new TestPositionComponent());
|
||||
|
||||
// 应该不抛出异常
|
||||
expect(() => {
|
||||
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息', () => {
|
||||
it('应该包含protobuf统计信息', () => {
|
||||
const stats = snapshotManager.getCacheStats();
|
||||
|
||||
expect(stats).toHaveProperty('snapshotCacheSize');
|
||||
expect(stats).toHaveProperty('protobufStats');
|
||||
expect(stats.protobufStats).toHaveProperty('registeredComponents');
|
||||
expect(stats.protobufStats).toHaveProperty('protobufAvailable');
|
||||
expect(stats.protobufStats!.protobufAvailable).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
113
packages/network/tests/Serialization/TsrpcSerializer.test.ts
Normal file
113
packages/network/tests/Serialization/TsrpcSerializer.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { TsrpcSerializer, SyncField } from '../../src/Serialization';
|
||||
import { TsrpcSerializable } from '../../src/Serialization/TsrpcDecorators';
|
||||
|
||||
@TsrpcSerializable()
|
||||
class TestComponent extends Component {
|
||||
@SyncField()
|
||||
public health: number = 100;
|
||||
|
||||
@SyncField()
|
||||
public name: string = 'Test';
|
||||
|
||||
@SyncField()
|
||||
public isActive: boolean = true;
|
||||
}
|
||||
|
||||
describe('TsrpcSerializer', () => {
|
||||
let serializer: TsrpcSerializer;
|
||||
let testComponent: TestComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
serializer = TsrpcSerializer.getInstance();
|
||||
testComponent = new TestComponent();
|
||||
testComponent.health = 80;
|
||||
testComponent.name = 'Player';
|
||||
testComponent.isActive = false;
|
||||
});
|
||||
|
||||
describe('序列化', () => {
|
||||
it('应该能序列化TSRPC组件', () => {
|
||||
const result = serializer.serialize(testComponent);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.type).toBe('tsrpc');
|
||||
expect(result?.componentType).toBe('TestComponent');
|
||||
expect(result?.data).toBeInstanceOf(Uint8Array);
|
||||
expect(result?.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('不支持的组件应该返回null', () => {
|
||||
// 创建一个没有装饰器的组件类
|
||||
class UnsupportedComponent extends Component {}
|
||||
const unsupportedComponent = new UnsupportedComponent();
|
||||
const result = serializer.serialize(unsupportedComponent);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('反序列化', () => {
|
||||
it('应该能反序列化TSRPC数据', () => {
|
||||
// 先序列化
|
||||
const serializedData = serializer.serialize(testComponent);
|
||||
expect(serializedData).not.toBeNull();
|
||||
|
||||
// 再反序列化
|
||||
const deserializedComponent = serializer.deserialize(serializedData!, TestComponent);
|
||||
|
||||
expect(deserializedComponent).not.toBeNull();
|
||||
expect(deserializedComponent?.health).toBe(80);
|
||||
expect(deserializedComponent?.name).toBe('Player');
|
||||
expect(deserializedComponent?.isActive).toBe(false);
|
||||
});
|
||||
|
||||
it('错误的数据类型应该返回null', () => {
|
||||
const invalidData = {
|
||||
type: 'json' as const,
|
||||
componentType: 'TestComponent',
|
||||
data: {},
|
||||
size: 0
|
||||
};
|
||||
|
||||
const result = serializer.deserialize(invalidData);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息', () => {
|
||||
it('应该正确更新统计信息', () => {
|
||||
const initialStats = serializer.getStats();
|
||||
|
||||
// 执行序列化
|
||||
serializer.serialize(testComponent);
|
||||
|
||||
const afterSerializeStats = serializer.getStats();
|
||||
expect(afterSerializeStats.serializeCount).toBe(initialStats.serializeCount + 1);
|
||||
|
||||
// 执行反序列化
|
||||
const serializedData = serializer.serialize(testComponent);
|
||||
if (serializedData) {
|
||||
serializer.deserialize(serializedData, TestComponent);
|
||||
}
|
||||
|
||||
const finalStats = serializer.getStats();
|
||||
expect(finalStats.deserializeCount).toBe(initialStats.deserializeCount + 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能功能', () => {
|
||||
it('应该正确计算序列化大小', () => {
|
||||
const initialStats = serializer.getStats();
|
||||
|
||||
// 执行序列化
|
||||
const result = serializer.serialize(testComponent);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.size).toBeGreaterThan(0);
|
||||
|
||||
const finalStats = serializer.getStats();
|
||||
expect(finalStats.averageSerializedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,7 @@
|
||||
*/
|
||||
|
||||
// 导入所有测试
|
||||
import './ProtobufDecorators.test';
|
||||
import './ProtobufSerializer.test';
|
||||
import './SnapshotManagerIntegration.test';
|
||||
import './Performance.test';
|
||||
import './TsrpcSerializer.test';
|
||||
|
||||
// 这个文件确保所有序列化相关的测试都被包含在测试套件中
|
||||
describe('序列化模块集成测试', () => {
|
||||
|
||||
Reference in New Issue
Block a user