使用Lerna 和 monorepo管理项目结构

This commit is contained in:
YHH
2025-08-07 13:29:12 +08:00
parent 4479f0fab0
commit ea8523be35
135 changed files with 7058 additions and 372 deletions

View File

@@ -0,0 +1,441 @@
/**
* Protobuf序列化性能测试
*/
import { Component } from '../../../src/ECS/Component';
import { Entity } from '../../../src/ECS/Entity';
import { Scene } from '../../../src/ECS/Scene';
import { SnapshotManager } from '../../../src/Utils/Snapshot/SnapshotManager';
import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer';
import {
ProtoSerializable,
ProtoFloat,
ProtoInt32,
ProtoString,
ProtoBool
} from '../../../src/Utils/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); // 模拟32字节的编码数据
mockEncodedData.fill(1);
return {
parse: jest.fn().mockReturnValue({
root: {
lookupType: jest.fn().mockImplementation((typeName: string) => ({
verify: jest.fn().mockReturnValue(null),
create: jest.fn().mockImplementation((data) => data),
encode: jest.fn().mockReturnValue({
finish: jest.fn().mockReturnValue(mockEncodedData)
}),
decode: jest.fn().mockReturnValue({
x: 10, y: 20, z: 30,
vx: 1, vy: 2, vz: 3,
maxHealth: 100, currentHealth: 80, isDead: false, regenerationRate: 0.5,
name: 'TestPlayer', level: 5, experience: 1000, score: 5000, isOnline: true
}),
toObject: jest.fn().mockImplementation((message) => message)
}))
}
})
};
};
describe('Protobuf序列化性能测试', () => {
let protobufSerializer: ProtobufSerializer;
let snapshotManager: SnapshotManager;
let scene: Scene;
beforeEach(() => {
protobufSerializer = ProtobufSerializer.getInstance();
protobufSerializer.initialize(createMockProtobuf());
snapshotManager = new SnapshotManager();
snapshotManager.initializeProtobuf(createMockProtobuf());
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
});
});
});

View File

@@ -0,0 +1,294 @@
/**
* Protobuf装饰器测试
*/
import { Component } from '../../../src/ECS/Component';
import {
ProtoSerializable,
ProtoField,
ProtoFieldType,
ProtoFloat,
ProtoInt32,
ProtoString,
ProtoBool,
ProtobufRegistry,
isProtoSerializable,
getProtoName
} from '../../../src/Utils/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).toBe(3);
const xField = definition!.fields.get('x');
expect(xField).toEqual({
fieldNumber: 1,
type: ProtoFieldType.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: ProtoFieldType.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).toBe(4);
const nameField = definition!.fields.get('name');
expect(nameField!.type).toBe(ProtoFieldType.STRING);
const levelField = definition!.fields.get('level');
expect(levelField!.type).toBe(ProtoFieldType.INT32);
const healthField = definition!.fields.get('health');
expect(healthField!.type).toBe(ProtoFieldType.INT32);
const isAliveField = definition!.fields.get('isAlive');
expect(isAliveField!.type).toBe(ProtoFieldType.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(ProtoFieldType.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(ProtoFieldType.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(ProtoFieldType.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(ProtoFieldType.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(2);
expect(allComponents.has('TestPosition')).toBe(true);
expect(allComponents.has('TestPlayer')).toBe(true);
});
});
describe('字段选项', () => {
it('应该支持repeated字段', () => {
@ProtoSerializable('RepeatedTest')
class RepeatedTestComponent extends Component {
@ProtoField(1, ProtoFieldType.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, ProtoFieldType.STRING, { optional: true })
public optionalValue?: string;
}
const definition = registry.getComponentDefinition('OptionalTest');
const field = definition!.fields.get('optionalValue');
expect(field!.optional).toBe(true);
});
});
});

View File

@@ -0,0 +1,315 @@
/**
* Protobuf序列化器测试
*/
import { Component } from '../../../src/ECS/Component';
import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer';
import { SerializedData } from '../../../src/Utils/Serialization/SerializationTypes';
import {
ProtoSerializable,
ProtoFloat,
ProtoInt32,
ProtoString,
ProtoBool,
ProtobufRegistry
} from '../../../src/Utils/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
const mockProtobuf = {
parse: jest.fn().mockReturnValue({
root: {
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)
};
})
}
})
};
describe('ProtobufSerializer', () => {
let serializer: ProtobufSerializer;
beforeEach(() => {
serializer = ProtobufSerializer.getInstance();
// 重置mock
jest.clearAllMocks();
});
describe('初始化', () => {
it('应该正确初始化protobuf支持', () => {
serializer.initialize(mockProtobuf);
expect(mockProtobuf.parse).toHaveBeenCalled();
expect(serializer.canSerialize(new PositionComponent())).toBe(true);
});
it('没有初始化时应该无法序列化protobuf组件', () => {
const newSerializer = new (ProtobufSerializer as any)();
expect(newSerializer.canSerialize(new PositionComponent())).toBe(false);
});
});
describe('序列化', () => {
beforeEach(() => {
serializer.initialize(mockProtobuf);
});
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('应该回退到JSON序列化非protobuf组件', () => {
const custom = new CustomComponent();
const result = serializer.serialize(custom);
expect(result.type).toBe('json');
expect(result.componentType).toBe('CustomComponent');
expect(result.data).toEqual(custom.serialize());
});
it('protobuf序列化失败时应该回退到JSON', () => {
// 模拟protobuf验证失败
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
mockType.verify.mockReturnValue('验证失败');
const position = new PositionComponent(10, 20, 30);
const result = serializer.serialize(position);
expect(result.type).toBe('json');
});
});
describe('反序列化', () => {
beforeEach(() => {
serializer.initialize(mockProtobuf);
});
it('应该正确反序列化protobuf数据', () => {
const position = new PositionComponent();
const serializedData: SerializedData = {
type: 'protobuf',
componentType: 'PositionComponent',
data: new Uint8Array([1, 2, 3, 4]),
size: 4
};
serializer.deserialize(position, serializedData);
// 验证decode和toObject被调用
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
expect(mockType.decode).toHaveBeenCalled();
expect(mockType.toObject).toHaveBeenCalled();
});
it('应该正确反序列化JSON数据', () => {
const custom = new CustomComponent();
const originalData = custom.serialize();
const serializedData: SerializedData = {
type: 'json',
componentType: 'CustomComponent',
data: originalData,
size: 100
};
// 修改组件数据
custom.customData.settings.volume = 0.5;
// 反序列化
serializer.deserialize(custom, serializedData);
// 验证数据被恢复
expect(custom.customData.settings.volume).toBe(0.8);
});
it('应该处理反序列化错误', () => {
const position = new PositionComponent();
const invalidData: SerializedData = {
type: 'protobuf',
componentType: 'PositionComponent',
data: new Uint8Array([255, 255, 255, 255]), // 无效数据
size: 4
};
// 模拟解码失败
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
mockType.decode.mockImplementation(() => {
throw new Error('解码失败');
});
// 应该不抛出异常
expect(() => {
serializer.deserialize(position, invalidData);
}).not.toThrow();
});
});
describe('统计信息', () => {
it('应该返回正确的统计信息', () => {
serializer.initialize(mockProtobuf);
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(false);
});
});
describe('边界情况', () => {
beforeEach(() => {
serializer.initialize(mockProtobuf);
});
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('应该处理循环引用', () => {
const custom = new CustomComponent();
// 创建循环引用
(custom as any).circular = custom;
const result = serializer.serialize(custom);
expect(result.type).toBe('json');
});
it('应该处理非常大的数值', () => {
const position = new PositionComponent(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, 0);
const result = serializer.serialize(position);
expect(result).toBeDefined();
});
});
});

View File

@@ -0,0 +1,433 @@
/**
* Protobuf序列化器边界情况测试
*/
import { Component } from '../../../src/ECS/Component';
import { BigIntFactory } from '../../../src/ECS/Utils/BigIntCompatibility';
import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer';
import {
ProtoSerializable,
ProtoFloat,
ProtoInt32,
ProtoString,
ProtoBool,
ProtoBytes,
ProtoTimestamp,
ProtoDouble,
ProtoInt64,
ProtoStruct
} from '../../../src/Utils/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 = {
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)
};
}),
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
}))
};
}
return null;
})
}
})
};
describe('ProtobufSerializer边界情况测试', () => {
let serializer: ProtobufSerializer;
beforeEach(() => {
serializer = ProtobufSerializer.getInstance();
serializer.initialize(mockProtobuf);
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');
// 循环引用应该抛出错误不再回退到JSON序列化
expect(() => {
serializer.serialize(component);
}).toThrow();
});
});
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
};
// 不应该抛出异常
expect(() => {
serializer.deserialize(component, serializedData);
}).not.toThrow();
});
});
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');
});
});
});

View File

@@ -0,0 +1,393 @@
/**
* 真实 Protobuf 序列化性能测试
* 使用实际的 protobufjs 库进行性能对比
*/
import 'reflect-metadata';
import { Component } from '../../../src/ECS/Component';
import {
ProtoSerializable,
ProtoFloat,
ProtoInt32,
ProtoString,
ProtoBool,
ProtobufRegistry
} from '../../../src/Utils/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);
});
});
});

View File

@@ -0,0 +1,371 @@
/**
* SnapshotManager与Protobuf序列化集成测试
*/
import { Entity } from '../../../src/ECS/Entity';
import { Scene } from '../../../src/ECS/Scene';
import { Component } from '../../../src/ECS/Component';
import { SnapshotManager } from '../../../src/Utils/Snapshot/SnapshotManager';
import {
ProtoSerializable,
ProtoFloat,
ProtoInt32,
ProtoString,
ProtoBool
} from '../../../src/Utils/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);
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();
// 验证protobuf的decode方法被调用
expect(mockProtobuf.parse().root.lookupType).toHaveBeenCalled();
});
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);
});
});
});

View File

@@ -0,0 +1,17 @@
/**
* 序列化模块集成测试
*/
// 导入所有测试
import './ProtobufDecorators.test';
import './ProtobufSerializer.test';
import './SnapshotManagerIntegration.test';
import './Performance.test';
// 这个文件确保所有序列化相关的测试都被包含在测试套件中
describe('序列化模块集成测试', () => {
it('应该包含所有序列化测试', () => {
// 这个测试确保模块正确加载
expect(true).toBe(true);
});
});

View File

@@ -0,0 +1,72 @@
/**
* Jest 测试全局设置文件
*
* 此文件在每个测试文件执行前运行,用于设置全局测试环境
*/
// 设置测试超时时间(毫秒)
jest.setTimeout(10000);
// 模拟控制台方法以减少测试输出噪音
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
const originalConsoleError = console.error;
// 在测试环境中可以选择性地静默某些日志
beforeAll(() => {
// 可以在这里设置全局的模拟或配置
});
afterAll(() => {
// 清理全局资源
});
// 每个测试前的清理
beforeEach(() => {
// 清理定时器
jest.clearAllTimers();
});
afterEach(() => {
// 恢复所有模拟
jest.restoreAllMocks();
});
// 导出测试工具函数
export const TestUtils = {
/**
* 创建测试用的延迟
* @param ms 延迟毫秒数
*/
delay: (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms));
},
/**
* 等待条件满足
* @param condition 条件函数
* @param timeout 超时时间(毫秒)
* @param interval 检查间隔(毫秒)
*/
waitFor: async (
condition: () => boolean,
timeout: number = 5000,
interval: number = 10
): Promise<void> => {
const start = Date.now();
while (!condition() && Date.now() - start < timeout) {
await TestUtils.delay(interval);
}
if (!condition()) {
throw new Error(`等待条件超时 (${timeout}ms)`);
}
},
/**
* 模拟时间前进
* @param ms 前进的毫秒数
*/
advanceTime: (ms: number): void => {
jest.advanceTimersByTime(ms);
}
};