优化IdentifierPool - 世代式ID池管理器
This commit is contained in:
76
.github/workflows/ci.yml
vendored
Normal file
76
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, main, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [18.x, 20.x, 22.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run tests with coverage
|
||||||
|
run: npm run test:ci
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
if: matrix.node-version == '20.x'
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
file: ./coverage/lcov.info
|
||||||
|
flags: unittests
|
||||||
|
name: codecov-umbrella
|
||||||
|
fail_ci_if_error: false
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Build npm package
|
||||||
|
run: npm run build:npm
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-artifacts
|
||||||
|
path: |
|
||||||
|
bin/
|
||||||
|
dist/
|
||||||
|
retention-days: 7
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# ECS Framework
|
# ECS Framework
|
||||||
|
|
||||||
|
[](https://github.com/esengine/ecs-framework/actions)
|
||||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
|||||||
Submodule extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen updated: eff68e895a...fb7d5bbb01
Submodule extensions/cocos/cocos-ecs/extensions/utilityai_designer deleted from 045748ffc3
39
jest.config.js
Normal file
39
jest.config.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>/tests', '<rootDir>/src'],
|
||||||
|
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
||||||
|
collectCoverage: false,
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.ts',
|
||||||
|
'!src/index.ts',
|
||||||
|
'!src/**/index.ts',
|
||||||
|
'!**/*.d.ts',
|
||||||
|
'!src/**/*.test.ts',
|
||||||
|
'!src/**/*.spec.ts'
|
||||||
|
],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
coverageReporters: ['text', 'lcov', 'html'],
|
||||||
|
verbose: true,
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': ['ts-jest', {
|
||||||
|
tsconfig: 'tsconfig.test.json',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
|
},
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||||
|
// 测试超时设置
|
||||||
|
testTimeout: 10000,
|
||||||
|
// 清除模块缓存
|
||||||
|
clearMocks: true,
|
||||||
|
restoreMocks: true,
|
||||||
|
// 忽略某些模块
|
||||||
|
modulePathIgnorePatterns: [
|
||||||
|
'<rootDir>/bin/',
|
||||||
|
'<rootDir>/dist/',
|
||||||
|
'<rootDir>/node_modules/'
|
||||||
|
]
|
||||||
|
};
|
||||||
4113
package-lock.json
generated
4113
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -30,7 +30,12 @@
|
|||||||
"publish:patch": "npm version patch && npm run build:npm && cd dist && npm publish",
|
"publish:patch": "npm version patch && npm run build:npm && cd dist && npm publish",
|
||||||
"publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish",
|
"publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish",
|
||||||
"publish:major": "npm version major && npm run build:npm && cd dist && npm publish",
|
"publish:major": "npm version major && npm run build:npm && cd dist && npm publish",
|
||||||
"publish:npm": "npm run build:npm && cd dist && npm publish"
|
"publish:npm": "npm run build:npm && cd dist && npm publish",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:coverage": "jest --coverage",
|
||||||
|
"test:ci": "jest --ci --coverage",
|
||||||
|
"test:clear": "jest --clearCache"
|
||||||
},
|
},
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -38,10 +43,14 @@
|
|||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"rollup": "^4.42.0",
|
"rollup": "^4.42.0",
|
||||||
"rollup-plugin-dts": "^6.2.1",
|
"rollup-plugin-dts": "^6.2.1",
|
||||||
|
"ts-jest": "^29.4.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ export class EntityList {
|
|||||||
|
|
||||||
// 更新名称索引
|
// 更新名称索引
|
||||||
this.updateNameIndex(entity, false);
|
this.updateNameIndex(entity, false);
|
||||||
|
|
||||||
|
// 回收实体ID到ID池
|
||||||
|
if (this._scene && this._scene.identifierPool) {
|
||||||
|
this._scene.identifierPool.checkIn(entity.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,10 +93,21 @@ export class EntityList {
|
|||||||
* 移除所有实体
|
* 移除所有实体
|
||||||
*/
|
*/
|
||||||
public removeAllEntities(): void {
|
public removeAllEntities(): void {
|
||||||
|
// 收集所有实体ID用于回收
|
||||||
|
const idsToRecycle: number[] = [];
|
||||||
|
|
||||||
for (let i = this.buffer.length - 1; i >= 0; i--) {
|
for (let i = this.buffer.length - 1; i >= 0; i--) {
|
||||||
|
idsToRecycle.push(this.buffer[i].id);
|
||||||
this.buffer[i].destroy();
|
this.buffer[i].destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量回收ID
|
||||||
|
if (this._scene && this._scene.identifierPool) {
|
||||||
|
for (const id of idsToRecycle) {
|
||||||
|
this._scene.identifierPool.checkIn(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.buffer.length = 0;
|
this.buffer.length = 0;
|
||||||
this._idToEntity.clear();
|
this._idToEntity.clear();
|
||||||
this._nameToEntities.clear();
|
this._nameToEntities.clear();
|
||||||
|
|||||||
@@ -1,26 +1,405 @@
|
|||||||
/**
|
/**
|
||||||
* ID池管理器
|
* 世代式ID池管理器
|
||||||
* 用于管理实体ID的分配和回收
|
*
|
||||||
|
* 用于管理实体ID的分配和回收,支持世代版本控制以防止悬空引用问题。
|
||||||
|
* 世代式ID由索引和版本组成,当ID被回收时版本会递增,确保旧引用失效。
|
||||||
|
*
|
||||||
|
* 支持动态扩展,理论上可以支持到65535个索引(16位),每个索引65535个版本(16位)。
|
||||||
|
* 总计可以处理超过42亿个独特的ID组合,完全满足ECS大规模实体需求。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const pool = new IdentifierPool();
|
||||||
|
*
|
||||||
|
* // 分配ID
|
||||||
|
* const id = pool.checkOut(); // 例如: 65536 (版本1,索引0)
|
||||||
|
*
|
||||||
|
* // 回收ID
|
||||||
|
* pool.checkIn(id);
|
||||||
|
*
|
||||||
|
* // 验证ID是否有效
|
||||||
|
* const isValid = pool.isValid(id); // false,因为版本已递增
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export class IdentifierPool {
|
export class IdentifierPool {
|
||||||
private _nextAvailableId = 0;
|
/**
|
||||||
private _ids: number[] = [];
|
* 下一个可用的索引
|
||||||
|
*/
|
||||||
|
private _nextAvailableIndex = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空闲的索引列表
|
||||||
|
*/
|
||||||
|
private _freeIndices: number[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个索引对应的世代版本
|
||||||
|
* 动态扩展的Map,按需分配内存
|
||||||
|
*/
|
||||||
|
private _generations = new Map<number, number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 延迟回收队列
|
||||||
|
* 防止在同一帧内立即重用ID,避免时序问题
|
||||||
|
*/
|
||||||
|
private _pendingRecycle: Array<{
|
||||||
|
index: number;
|
||||||
|
generation: number;
|
||||||
|
timestamp: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 延迟回收时间(毫秒)
|
||||||
|
*/
|
||||||
|
private _recycleDelay: number = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大索引限制(16位)
|
||||||
|
* 这是框架设计选择:16位索引 + 16位版本 = 32位ID,确保高效位操作
|
||||||
|
* 不是硬件限制,而是性能和内存效率的权衡
|
||||||
|
*/
|
||||||
|
private static readonly MAX_INDEX = 0xFFFF; // 65535
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大世代限制(16位)
|
||||||
|
*/
|
||||||
|
private static readonly MAX_GENERATION = 0xFFFF; // 65535
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内存扩展块大小
|
||||||
|
* 当需要更多内存时,一次性预分配的索引数量
|
||||||
|
*/
|
||||||
|
private _expansionBlockSize: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计信息
|
||||||
|
*/
|
||||||
|
private _stats = {
|
||||||
|
totalAllocated: 0,
|
||||||
|
totalRecycled: 0,
|
||||||
|
currentActive: 0,
|
||||||
|
memoryExpansions: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param recycleDelay 延迟回收时间(毫秒),默认为100ms
|
||||||
|
* @param expansionBlockSize 内存扩展块大小,默认为1024
|
||||||
|
*/
|
||||||
|
constructor(recycleDelay: number = 100, expansionBlockSize: number = 1024) {
|
||||||
|
this._recycleDelay = recycleDelay;
|
||||||
|
this._expansionBlockSize = expansionBlockSize;
|
||||||
|
|
||||||
|
// 预分配第一个块的世代信息
|
||||||
|
this._preAllocateGenerations(0, this._expansionBlockSize);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一个可用的ID
|
* 获取一个可用的ID
|
||||||
|
*
|
||||||
|
* 返回一个32位ID,高16位为世代版本,低16位为索引。
|
||||||
|
*
|
||||||
|
* @returns 新分配的实体ID
|
||||||
|
* @throws {Error} 当达到索引限制时抛出错误
|
||||||
*/
|
*/
|
||||||
public checkOut(): number {
|
public checkOut(): number {
|
||||||
if (this._ids.length > 0) {
|
// 处理延迟回收队列
|
||||||
return this._ids.pop()!;
|
this._processDelayedRecycle();
|
||||||
|
|
||||||
|
let index: number;
|
||||||
|
|
||||||
|
if (this._freeIndices.length > 0) {
|
||||||
|
// 重用回收的索引
|
||||||
|
index = this._freeIndices.pop()!;
|
||||||
|
} else {
|
||||||
|
// 分配新索引
|
||||||
|
if (this._nextAvailableIndex > IdentifierPool.MAX_INDEX) {
|
||||||
|
throw new Error(
|
||||||
|
`实体索引已达到框架设计限制 (${IdentifierPool.MAX_INDEX})。` +
|
||||||
|
`这意味着您已经分配了超过65535个不同的实体索引。` +
|
||||||
|
`这是16位索引设计的限制,考虑优化实体回收策略或升级到64位ID设计。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
index = this._nextAvailableIndex++;
|
||||||
|
|
||||||
|
// 按需扩展世代存储
|
||||||
|
this._ensureGenerationCapacity(index);
|
||||||
}
|
}
|
||||||
return this._nextAvailableId++;
|
|
||||||
|
const generation = this._generations.get(index) || 1;
|
||||||
|
this._stats.totalAllocated++;
|
||||||
|
this._stats.currentActive++;
|
||||||
|
|
||||||
|
return this._packId(index, generation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 回收一个ID
|
* 回收一个ID
|
||||||
* @param id 要回收的ID
|
*
|
||||||
|
* 验证ID的有效性后,将其加入延迟回收队列。
|
||||||
|
* ID不会立即可重用,而是在延迟时间后才真正回收。
|
||||||
|
*
|
||||||
|
* @param id 要回收的实体ID
|
||||||
|
* @returns 是否成功回收(ID是否有效且未被重复回收)
|
||||||
*/
|
*/
|
||||||
public checkIn(id: number): void {
|
public checkIn(id: number): boolean {
|
||||||
this._ids.push(id);
|
const index = this._unpackIndex(id);
|
||||||
|
const generation = this._unpackGeneration(id);
|
||||||
|
|
||||||
|
// 验证ID有效性
|
||||||
|
if (!this._isValidId(index, generation)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经在待回收队列中
|
||||||
|
const alreadyPending = this._pendingRecycle.some(
|
||||||
|
item => item.index === index && item.generation === generation
|
||||||
|
);
|
||||||
|
|
||||||
|
if (alreadyPending) {
|
||||||
|
return false; // 已经在回收队列中,拒绝重复回收
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加入延迟回收队列
|
||||||
|
this._pendingRecycle.push({
|
||||||
|
index,
|
||||||
|
generation,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
this._stats.currentActive--;
|
||||||
|
this._stats.totalRecycled++;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证ID是否有效
|
||||||
|
*
|
||||||
|
* 检查ID的索引和世代版本是否匹配当前状态。
|
||||||
|
*
|
||||||
|
* @param id 要验证的实体ID
|
||||||
|
* @returns ID是否有效
|
||||||
|
*/
|
||||||
|
public isValid(id: number): boolean {
|
||||||
|
const index = this._unpackIndex(id);
|
||||||
|
const generation = this._unpackGeneration(id);
|
||||||
|
return this._isValidId(index, generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统计信息
|
||||||
|
*
|
||||||
|
* @returns 池的当前状态统计
|
||||||
|
*/
|
||||||
|
public getStats(): {
|
||||||
|
/** 已分配的总索引数 */
|
||||||
|
totalAllocated: number;
|
||||||
|
/** 总计回收次数 */
|
||||||
|
totalRecycled: number;
|
||||||
|
/** 当前活跃实体数 */
|
||||||
|
currentActive: number;
|
||||||
|
/** 当前空闲的索引数 */
|
||||||
|
currentlyFree: number;
|
||||||
|
/** 等待回收的ID数 */
|
||||||
|
pendingRecycle: number;
|
||||||
|
/** 理论最大实体数(设计限制) */
|
||||||
|
maxPossibleEntities: number;
|
||||||
|
/** 当前使用的最大索引 */
|
||||||
|
maxUsedIndex: number;
|
||||||
|
/** 内存使用(字节) */
|
||||||
|
memoryUsage: number;
|
||||||
|
/** 内存扩展次数 */
|
||||||
|
memoryExpansions: number;
|
||||||
|
/** 平均世代版本 */
|
||||||
|
averageGeneration: number;
|
||||||
|
/** 世代存储大小 */
|
||||||
|
generationStorageSize: number;
|
||||||
|
} {
|
||||||
|
// 计算平均世代版本
|
||||||
|
let totalGeneration = 0;
|
||||||
|
let generationCount = 0;
|
||||||
|
|
||||||
|
for (const [index, generation] of this._generations) {
|
||||||
|
if (index < this._nextAvailableIndex) {
|
||||||
|
totalGeneration += generation;
|
||||||
|
generationCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const averageGeneration = generationCount > 0
|
||||||
|
? totalGeneration / generationCount
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalAllocated: this._stats.totalAllocated,
|
||||||
|
totalRecycled: this._stats.totalRecycled,
|
||||||
|
currentActive: this._stats.currentActive,
|
||||||
|
currentlyFree: this._freeIndices.length,
|
||||||
|
pendingRecycle: this._pendingRecycle.length,
|
||||||
|
maxPossibleEntities: IdentifierPool.MAX_INDEX + 1,
|
||||||
|
maxUsedIndex: this._nextAvailableIndex - 1,
|
||||||
|
memoryUsage: this._calculateMemoryUsage(),
|
||||||
|
memoryExpansions: this._stats.memoryExpansions,
|
||||||
|
averageGeneration: Math.round(averageGeneration * 100) / 100,
|
||||||
|
generationStorageSize: this._generations.size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制执行延迟回收处理
|
||||||
|
*
|
||||||
|
* 在某些情况下可能需要立即处理延迟回收队列,
|
||||||
|
* 比如内存压力大或者需要精确的统计信息时。
|
||||||
|
*/
|
||||||
|
public forceProcessDelayedRecycle(): void {
|
||||||
|
this._processDelayedRecycle(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期的延迟回收项
|
||||||
|
*
|
||||||
|
* 将超过延迟时间的回收项真正回收到空闲列表中。
|
||||||
|
*
|
||||||
|
* @param forceAll 是否强制处理所有延迟回收项
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _processDelayedRecycle(forceAll: boolean = false): void {
|
||||||
|
if (this._pendingRecycle.length === 0) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const readyToRecycle: typeof this._pendingRecycle = [];
|
||||||
|
const stillPending: typeof this._pendingRecycle = [];
|
||||||
|
|
||||||
|
// 分离已到期和未到期的项
|
||||||
|
for (const item of this._pendingRecycle) {
|
||||||
|
if (forceAll || now - item.timestamp >= this._recycleDelay) {
|
||||||
|
readyToRecycle.push(item);
|
||||||
|
} else {
|
||||||
|
stillPending.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理到期的回收项
|
||||||
|
for (const item of readyToRecycle) {
|
||||||
|
// 再次验证ID有效性(防止重复回收)
|
||||||
|
if (this._isValidId(item.index, item.generation)) {
|
||||||
|
// 递增世代版本
|
||||||
|
let newGeneration = item.generation + 1;
|
||||||
|
|
||||||
|
// 防止世代版本溢出
|
||||||
|
if (newGeneration > IdentifierPool.MAX_GENERATION) {
|
||||||
|
newGeneration = 1; // 重置为1而不是0
|
||||||
|
}
|
||||||
|
|
||||||
|
this._generations.set(item.index, newGeneration);
|
||||||
|
|
||||||
|
// 添加到空闲列表
|
||||||
|
this._freeIndices.push(item.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新待回收队列
|
||||||
|
this._pendingRecycle = stillPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预分配世代信息
|
||||||
|
*
|
||||||
|
* @param startIndex 起始索引
|
||||||
|
* @param count 分配数量
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _preAllocateGenerations(startIndex: number, count: number): void {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const index = startIndex + i;
|
||||||
|
if (index <= IdentifierPool.MAX_INDEX) {
|
||||||
|
this._generations.set(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._stats.memoryExpansions++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保指定索引的世代信息存在
|
||||||
|
*
|
||||||
|
* @param index 索引
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _ensureGenerationCapacity(index: number): void {
|
||||||
|
if (!this._generations.has(index)) {
|
||||||
|
// 计算需要扩展的起始位置
|
||||||
|
const expansionStart = Math.floor(index / this._expansionBlockSize) * this._expansionBlockSize;
|
||||||
|
|
||||||
|
// 预分配一个块
|
||||||
|
this._preAllocateGenerations(expansionStart, this._expansionBlockSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算内存使用量
|
||||||
|
*
|
||||||
|
* @returns 内存使用字节数
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _calculateMemoryUsage(): number {
|
||||||
|
const generationMapSize = this._generations.size * 16; // Map overhead + number pair
|
||||||
|
const freeIndicesSize = this._freeIndices.length * 8;
|
||||||
|
const pendingRecycleSize = this._pendingRecycle.length * 32;
|
||||||
|
|
||||||
|
return generationMapSize + freeIndicesSize + pendingRecycleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包索引和世代为32位ID
|
||||||
|
*
|
||||||
|
* @param index 索引(16位)
|
||||||
|
* @param generation 世代版本(16位)
|
||||||
|
* @returns 打包后的32位ID
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _packId(index: number, generation: number): number {
|
||||||
|
return (generation << 16) | index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从ID中解包索引
|
||||||
|
*
|
||||||
|
* @param id 32位ID
|
||||||
|
* @returns 索引部分(16位)
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _unpackIndex(id: number): number {
|
||||||
|
return id & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从ID中解包世代版本
|
||||||
|
*
|
||||||
|
* @param id 32位ID
|
||||||
|
* @returns 世代版本部分(16位)
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _unpackGeneration(id: number): number {
|
||||||
|
return (id >>> 16) & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部ID有效性检查
|
||||||
|
*
|
||||||
|
* @param index 索引
|
||||||
|
* @param generation 世代版本
|
||||||
|
* @returns 是否有效
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _isValidId(index: number, generation: number): boolean {
|
||||||
|
if (index < 0 || index >= this._nextAvailableIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentGeneration = this._generations.get(index);
|
||||||
|
return currentGeneration !== undefined && currentGeneration === generation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
414
tests/ECS/Utils/IdentifierPool.test.ts
Normal file
414
tests/ECS/Utils/IdentifierPool.test.ts
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
/**
|
||||||
|
* IdentifierPool 世代式ID池测试
|
||||||
|
*
|
||||||
|
* 测试实体ID的分配、回收、验证和世代版本控制功能
|
||||||
|
*/
|
||||||
|
import { IdentifierPool } from '../../../src/ECS/Utils/IdentifierPool';
|
||||||
|
import { TestUtils } from '../../setup';
|
||||||
|
|
||||||
|
describe('IdentifierPool 世代式ID池测试', () => {
|
||||||
|
let pool: IdentifierPool;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pool = new IdentifierPool();
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试基本功能
|
||||||
|
describe('基本功能测试', () => {
|
||||||
|
test('应该能创建IdentifierPool实例', () => {
|
||||||
|
expect(pool).toBeDefined();
|
||||||
|
expect(pool).toBeInstanceOf(IdentifierPool);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该能分配连续的ID', () => {
|
||||||
|
const id1 = pool.checkOut();
|
||||||
|
const id2 = pool.checkOut();
|
||||||
|
const id3 = pool.checkOut();
|
||||||
|
|
||||||
|
expect(id1).toBe(65536); // 世代1,索引0
|
||||||
|
expect(id2).toBe(65537); // 世代1,索引1
|
||||||
|
expect(id3).toBe(65538); // 世代1,索引2
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该能验证有效的ID', () => {
|
||||||
|
const id = pool.checkOut();
|
||||||
|
expect(pool.isValid(id)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该能获取统计信息', () => {
|
||||||
|
const id1 = pool.checkOut();
|
||||||
|
const id2 = pool.checkOut();
|
||||||
|
|
||||||
|
const stats = pool.getStats();
|
||||||
|
expect(stats.totalAllocated).toBe(2);
|
||||||
|
expect(stats.currentActive).toBe(2);
|
||||||
|
expect(stats.currentlyFree).toBe(0);
|
||||||
|
expect(stats.pendingRecycle).toBe(0);
|
||||||
|
expect(stats.maxPossibleEntities).toBe(65536); // 2^16
|
||||||
|
expect(stats.averageGeneration).toBe(1);
|
||||||
|
expect(stats.memoryUsage).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试回收功能
|
||||||
|
describe('ID回收功能测试', () => {
|
||||||
|
test('应该能回收有效的ID', () => {
|
||||||
|
const id = pool.checkOut();
|
||||||
|
const result = pool.checkIn(id);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
const stats = pool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBe(1);
|
||||||
|
expect(stats.currentActive).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该拒绝回收无效的ID', () => {
|
||||||
|
const invalidId = 999999;
|
||||||
|
const result = pool.checkIn(invalidId);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
|
||||||
|
const stats = pool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该拒绝重复回收同一个ID', () => {
|
||||||
|
const id = pool.checkOut();
|
||||||
|
|
||||||
|
const firstResult = pool.checkIn(id);
|
||||||
|
const secondResult = pool.checkIn(id);
|
||||||
|
|
||||||
|
expect(firstResult).toBe(true);
|
||||||
|
expect(secondResult).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试延迟回收
|
||||||
|
describe('延迟回收机制测试', () => {
|
||||||
|
test('应该支持延迟回收', () => {
|
||||||
|
const pool = new IdentifierPool(100); // 100ms延迟
|
||||||
|
|
||||||
|
const id = pool.checkOut();
|
||||||
|
pool.checkIn(id);
|
||||||
|
|
||||||
|
// 立即检查,ID应该还在延迟队列中
|
||||||
|
let stats = pool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBe(1);
|
||||||
|
expect(stats.currentlyFree).toBe(0);
|
||||||
|
|
||||||
|
// 模拟时间前进150ms
|
||||||
|
jest.advanceTimersByTime(150);
|
||||||
|
|
||||||
|
// 触发延迟回收处理(通过分配新ID)
|
||||||
|
pool.checkOut();
|
||||||
|
|
||||||
|
// 现在ID应该被真正回收了
|
||||||
|
stats = pool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBe(0);
|
||||||
|
expect(stats.currentlyFree).toBe(0); // 因为被重新分配了
|
||||||
|
});
|
||||||
|
|
||||||
|
test('延迟时间内ID应该仍然有效', () => {
|
||||||
|
const pool = new IdentifierPool(100);
|
||||||
|
|
||||||
|
const id = pool.checkOut();
|
||||||
|
pool.checkIn(id);
|
||||||
|
|
||||||
|
// 在延迟时间内,ID应该仍然有效
|
||||||
|
expect(pool.isValid(id)).toBe(true);
|
||||||
|
|
||||||
|
// 模拟时间前进150ms并触发处理
|
||||||
|
jest.advanceTimersByTime(150);
|
||||||
|
pool.checkOut(); // 触发延迟回收处理
|
||||||
|
|
||||||
|
// 现在ID应该无效了(世代已递增)
|
||||||
|
expect(pool.isValid(id)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该支持强制延迟回收处理', () => {
|
||||||
|
const id = pool.checkOut();
|
||||||
|
pool.checkIn(id);
|
||||||
|
|
||||||
|
// 在延迟时间内强制处理
|
||||||
|
pool.forceProcessDelayedRecycle();
|
||||||
|
|
||||||
|
// ID应该立即变为无效
|
||||||
|
expect(pool.isValid(id)).toBe(false);
|
||||||
|
|
||||||
|
const stats = pool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBe(0);
|
||||||
|
expect(stats.currentlyFree).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试世代版本控制
|
||||||
|
describe('世代版本控制测试', () => {
|
||||||
|
test('回收后的ID应该增加世代版本', () => {
|
||||||
|
const pool = new IdentifierPool(0); // 无延迟,立即回收
|
||||||
|
|
||||||
|
const originalId = pool.checkOut();
|
||||||
|
pool.checkIn(originalId);
|
||||||
|
|
||||||
|
// 分配新ID触发回收处理
|
||||||
|
const newId = pool.checkOut();
|
||||||
|
|
||||||
|
// 原ID应该无效
|
||||||
|
expect(pool.isValid(originalId)).toBe(false);
|
||||||
|
|
||||||
|
// 新ID应该有不同的世代版本
|
||||||
|
expect(newId).not.toBe(originalId);
|
||||||
|
expect(newId).toBe(131072); // 世代2,索引0
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该能重用回收的索引', () => {
|
||||||
|
const pool = new IdentifierPool(0);
|
||||||
|
|
||||||
|
const id1 = pool.checkOut(); // 索引0
|
||||||
|
const id2 = pool.checkOut(); // 索引1
|
||||||
|
|
||||||
|
pool.checkIn(id1);
|
||||||
|
|
||||||
|
const id3 = pool.checkOut(); // 应该重用索引0,但世代递增
|
||||||
|
|
||||||
|
expect(id3 & 0xFFFF).toBe(0); // 索引部分应该是0
|
||||||
|
expect(id3 >> 16).toBe(2); // 世代应该是2
|
||||||
|
});
|
||||||
|
|
||||||
|
test('世代版本溢出应该重置为1', () => {
|
||||||
|
const pool = new IdentifierPool(0);
|
||||||
|
|
||||||
|
// 手动设置一个即将溢出的世代
|
||||||
|
const id = pool.checkOut();
|
||||||
|
|
||||||
|
// 通过反射访问私有成员来模拟溢出情况
|
||||||
|
const generations = (pool as any)._generations;
|
||||||
|
generations.set(0, 65535); // 设置为最大值
|
||||||
|
|
||||||
|
pool.checkIn(id);
|
||||||
|
const newId = pool.checkOut();
|
||||||
|
|
||||||
|
// 世代应该重置为1而不是0
|
||||||
|
expect(newId >> 16).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试错误处理
|
||||||
|
describe('错误处理测试', () => {
|
||||||
|
test('超过最大索引数应该抛出错误', () => {
|
||||||
|
// 创建一个模拟的池,直接设置到达到限制
|
||||||
|
const pool = new IdentifierPool();
|
||||||
|
|
||||||
|
// 通过反射设置到达到限制(65536会触发错误)
|
||||||
|
(pool as any)._nextAvailableIndex = 65536;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
pool.checkOut();
|
||||||
|
}).toThrow('实体索引已达到硬件限制');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该能处理边界值', () => {
|
||||||
|
const pool = new IdentifierPool();
|
||||||
|
|
||||||
|
const id = pool.checkOut();
|
||||||
|
expect(id).toBe(65536); // 世代1,索引0
|
||||||
|
|
||||||
|
// 回收并重新分配
|
||||||
|
pool.checkIn(id);
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(200);
|
||||||
|
const newId = pool.checkOut();
|
||||||
|
|
||||||
|
expect(newId).toBe(131072); // 世代2,索引0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试动态扩展
|
||||||
|
describe('动态内存扩展测试', () => {
|
||||||
|
test('应该能动态扩展内存', () => {
|
||||||
|
const pool = new IdentifierPool(0, 10); // 小的扩展块用于测试
|
||||||
|
|
||||||
|
// 分配超过初始块大小的ID
|
||||||
|
const ids: number[] = [];
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
ids.push(pool.checkOut());
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ids.length).toBe(25);
|
||||||
|
|
||||||
|
// 验证所有ID都是唯一的
|
||||||
|
const uniqueIds = new Set(ids);
|
||||||
|
expect(uniqueIds.size).toBe(25);
|
||||||
|
|
||||||
|
// 检查内存扩展统计
|
||||||
|
const stats = pool.getStats();
|
||||||
|
expect(stats.memoryExpansions).toBeGreaterThan(1);
|
||||||
|
expect(stats.generationStorageSize).toBeGreaterThanOrEqual(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('内存扩展应该按块进行', () => {
|
||||||
|
const blockSize = 5;
|
||||||
|
const pool = new IdentifierPool(0, blockSize);
|
||||||
|
|
||||||
|
// 分配第一个块
|
||||||
|
for (let i = 0; i < blockSize; i++) {
|
||||||
|
pool.checkOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats = pool.getStats();
|
||||||
|
const initialExpansions = stats.memoryExpansions;
|
||||||
|
|
||||||
|
// 分配一个会触发新块的ID
|
||||||
|
pool.checkOut();
|
||||||
|
|
||||||
|
stats = pool.getStats();
|
||||||
|
expect(stats.memoryExpansions).toBe(initialExpansions + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试性能和内存
|
||||||
|
describe('性能和内存测试', () => {
|
||||||
|
test('应该能处理大量ID分配', () => {
|
||||||
|
const count = 10000; // 增加测试规模
|
||||||
|
const ids: number[] = [];
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
ids.push(pool.checkOut());
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
|
||||||
|
expect(ids.length).toBe(count);
|
||||||
|
expect(duration).toBeLessThan(1000); // 10k个ID应该在1秒内完成
|
||||||
|
|
||||||
|
// 验证所有ID都是唯一的
|
||||||
|
const uniqueIds = new Set(ids);
|
||||||
|
expect(uniqueIds.size).toBe(count);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该能处理大量回收操作', () => {
|
||||||
|
const count = 5000; // 增加测试规模
|
||||||
|
const ids: number[] = [];
|
||||||
|
|
||||||
|
// 分配ID
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
ids.push(pool.checkOut());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回收ID
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
pool.checkIn(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
|
||||||
|
expect(duration).toBeLessThan(500); // 5k个回收应该在500ms内完成
|
||||||
|
|
||||||
|
const stats = pool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBe(count);
|
||||||
|
expect(stats.currentActive).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('内存使用应该是合理的', () => {
|
||||||
|
const stats = pool.getStats();
|
||||||
|
const initialMemory = stats.memoryUsage;
|
||||||
|
|
||||||
|
// 分配大量ID
|
||||||
|
for (let i = 0; i < 5000; i++) {
|
||||||
|
pool.checkOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStats = pool.getStats();
|
||||||
|
const memoryIncrease = newStats.memoryUsage - initialMemory;
|
||||||
|
|
||||||
|
// 内存增长应该是合理的(动态分配应该更高效)
|
||||||
|
expect(memoryIncrease).toBeLessThan(5000 * 50); // 每个ID少于50字节
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试并发安全性(模拟)
|
||||||
|
describe('并发安全性测试', () => {
|
||||||
|
test('应该能处理并发分配', async () => {
|
||||||
|
const promises: Promise<number>[] = [];
|
||||||
|
|
||||||
|
// 模拟并发分配
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
promises.push(Promise.resolve(pool.checkOut()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = await Promise.all(promises);
|
||||||
|
|
||||||
|
// 所有ID应该是唯一的
|
||||||
|
const uniqueIds = new Set(ids);
|
||||||
|
expect(uniqueIds.size).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该能处理并发回收', async () => {
|
||||||
|
const ids: number[] = [];
|
||||||
|
|
||||||
|
// 先分配一些ID
|
||||||
|
for (let i = 0; i < 500; i++) {
|
||||||
|
ids.push(pool.checkOut());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟并发回收
|
||||||
|
const promises = ids.map(id => Promise.resolve(pool.checkIn(id)));
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
|
// 所有回收操作都应该成功
|
||||||
|
expect(results.every(result => result === true)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试统计信息
|
||||||
|
describe('统计信息测试', () => {
|
||||||
|
test('统计信息应该准确反映池状态', () => {
|
||||||
|
// 分配一些ID
|
||||||
|
const ids = [pool.checkOut(), pool.checkOut(), pool.checkOut()];
|
||||||
|
|
||||||
|
let stats = pool.getStats();
|
||||||
|
expect(stats.totalAllocated).toBe(3);
|
||||||
|
expect(stats.currentActive).toBe(3);
|
||||||
|
expect(stats.currentlyFree).toBe(0);
|
||||||
|
expect(stats.pendingRecycle).toBe(0);
|
||||||
|
|
||||||
|
// 回收一个ID
|
||||||
|
pool.checkIn(ids[0]);
|
||||||
|
|
||||||
|
stats = pool.getStats();
|
||||||
|
expect(stats.totalRecycled).toBe(1);
|
||||||
|
expect(stats.currentActive).toBe(2);
|
||||||
|
expect(stats.pendingRecycle).toBe(1);
|
||||||
|
|
||||||
|
// 强制处理延迟回收
|
||||||
|
pool.forceProcessDelayedRecycle();
|
||||||
|
|
||||||
|
stats = pool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBe(0);
|
||||||
|
expect(stats.currentlyFree).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该正确计算平均世代版本', () => {
|
||||||
|
const pool = new IdentifierPool(0); // 无延迟
|
||||||
|
|
||||||
|
// 分配、回收、再分配来增加世代
|
||||||
|
const id1 = pool.checkOut();
|
||||||
|
pool.checkIn(id1);
|
||||||
|
const id2 = pool.checkOut(); // 这会触发世代递增
|
||||||
|
|
||||||
|
const stats = pool.getStats();
|
||||||
|
expect(stats.averageGeneration).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
220
tests/integration/IdentifierPool.integration.test.ts
Normal file
220
tests/integration/IdentifierPool.integration.test.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* IdentifierPool 集成测试
|
||||||
|
*
|
||||||
|
* 测试新的世代式IdentifierPool与现有ECS系统的兼容性
|
||||||
|
*/
|
||||||
|
import { IdentifierPool } from '../../src/ECS/Utils/IdentifierPool';
|
||||||
|
import { Scene } from '../../src/ECS/Scene';
|
||||||
|
import { Entity } from '../../src/ECS/Entity';
|
||||||
|
|
||||||
|
describe('IdentifierPool 集成测试', () => {
|
||||||
|
let scene: Scene;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scene = new Scene();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('与Scene系统集成', () => {
|
||||||
|
test('Scene应该能使用新的IdentifierPool', () => {
|
||||||
|
// Scene内部使用IdentifierPool
|
||||||
|
expect(scene.identifierPool).toBeInstanceOf(IdentifierPool);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Scene应该能正常创建实体', () => {
|
||||||
|
const entity1 = scene.createEntity('TestEntity1');
|
||||||
|
const entity2 = scene.createEntity('TestEntity2');
|
||||||
|
const entity3 = scene.createEntity('TestEntity3');
|
||||||
|
|
||||||
|
expect(entity1).toBeInstanceOf(Entity);
|
||||||
|
expect(entity2).toBeInstanceOf(Entity);
|
||||||
|
expect(entity3).toBeInstanceOf(Entity);
|
||||||
|
|
||||||
|
// ID应该是唯一的
|
||||||
|
expect(entity1.id).not.toBe(entity2.id);
|
||||||
|
expect(entity2.id).not.toBe(entity3.id);
|
||||||
|
expect(entity1.id).not.toBe(entity3.id);
|
||||||
|
|
||||||
|
// 验证新的ID格式(世代式)
|
||||||
|
expect(entity1.id).toBeGreaterThan(65535); // 应该包含世代信息
|
||||||
|
expect(entity2.id).toBeGreaterThan(65535);
|
||||||
|
expect(entity3.id).toBeGreaterThan(65535);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('实体销毁应该能正确回收ID', () => {
|
||||||
|
const entity = scene.createEntity('ToDestroy');
|
||||||
|
const originalId = entity.id;
|
||||||
|
|
||||||
|
// 销毁实体
|
||||||
|
entity.destroy();
|
||||||
|
|
||||||
|
// 验证ID池统计
|
||||||
|
const stats = scene.identifierPool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// 在当前时间点,ID应该仍然有效(因为有延迟回收)
|
||||||
|
expect(scene.identifierPool.isValid(originalId)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('大量实体创建和销毁应该正常工作', () => {
|
||||||
|
const entities: Entity[] = [];
|
||||||
|
const count = 100;
|
||||||
|
|
||||||
|
// 创建大量实体
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
entities.push(scene.createEntity(`Entity_${i}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证实体数量
|
||||||
|
expect(entities.length).toBe(count);
|
||||||
|
|
||||||
|
// 验证所有ID唯一
|
||||||
|
const ids = entities.map(e => e.id);
|
||||||
|
const uniqueIds = new Set(ids);
|
||||||
|
expect(uniqueIds.size).toBe(count);
|
||||||
|
|
||||||
|
// 销毁一半实体
|
||||||
|
const toDestroy = entities.slice(0, 50);
|
||||||
|
toDestroy.forEach(entity => entity.destroy());
|
||||||
|
|
||||||
|
// 验证ID池状态
|
||||||
|
const stats = scene.identifierPool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBe(50);
|
||||||
|
expect(stats.totalAllocated).toBe(count);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('批量创建实体应该正常工作', () => {
|
||||||
|
const count = 50;
|
||||||
|
const entities = scene.createEntities(count, 'BatchEntity');
|
||||||
|
|
||||||
|
expect(entities.length).toBe(count);
|
||||||
|
|
||||||
|
// 验证所有实体都有有效ID
|
||||||
|
entities.forEach(entity => {
|
||||||
|
expect(scene.identifierPool.isValid(entity.id)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证ID唯一性
|
||||||
|
const ids = entities.map(e => e.id);
|
||||||
|
const uniqueIds = new Set(ids);
|
||||||
|
expect(uniqueIds.size).toBe(count);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('性能和内存验证', () => {
|
||||||
|
test('ID分配性能应该满足要求', () => {
|
||||||
|
const count = 1000;
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
scene.createEntity(`PerfTest_${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
|
||||||
|
// 1000个实体应该在100ms内创建完成
|
||||||
|
expect(duration).toBeLessThan(100);
|
||||||
|
|
||||||
|
// 验证内存使用合理(动态分配应该更高效)
|
||||||
|
const stats = scene.identifierPool.getStats();
|
||||||
|
expect(stats.memoryUsage).toBeLessThan(1000 * 100); // 每个实体少于100字节
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ID回收不应该影响性能', () => {
|
||||||
|
const entities: Entity[] = [];
|
||||||
|
const count = 500;
|
||||||
|
|
||||||
|
// 创建实体
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
entities.push(scene.createEntity(`RecycleTest_${i}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试回收性能
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
entities.forEach(entity => entity.destroy());
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
|
||||||
|
// 回收500个实体应该在50ms内完成
|
||||||
|
expect(duration).toBeLessThan(50);
|
||||||
|
|
||||||
|
const stats = scene.identifierPool.getStats();
|
||||||
|
expect(stats.pendingRecycle).toBe(count);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('向后兼容性', () => {
|
||||||
|
test('现有的Entity API应该继续工作', () => {
|
||||||
|
const entity = scene.createEntity('CompatTest');
|
||||||
|
|
||||||
|
// 基本属性应该存在
|
||||||
|
expect(typeof entity.id).toBe('number');
|
||||||
|
expect(typeof entity.name).toBe('string');
|
||||||
|
expect(entity.name).toBe('CompatTest');
|
||||||
|
|
||||||
|
// 基本方法应该工作
|
||||||
|
expect(typeof entity.destroy).toBe('function');
|
||||||
|
expect(typeof entity.addComponent).toBe('function');
|
||||||
|
expect(typeof entity.getComponent).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Scene的createEntity方法应该继续工作', () => {
|
||||||
|
// 传入名称
|
||||||
|
const entity1 = scene.createEntity('TestEntity');
|
||||||
|
expect(entity1).toBeInstanceOf(Entity);
|
||||||
|
|
||||||
|
const entity2 = scene.createEntity('Named');
|
||||||
|
expect(entity2.name).toBe('Named');
|
||||||
|
|
||||||
|
// 批量创建
|
||||||
|
const entities = scene.createEntities(5);
|
||||||
|
expect(entities.length).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('实体销毁应该继续工作', () => {
|
||||||
|
const entity = scene.createEntity('ToDestroy');
|
||||||
|
const initialCount = scene.entities.count;
|
||||||
|
|
||||||
|
entity.destroy();
|
||||||
|
|
||||||
|
expect(entity.isDestroyed).toBe(true);
|
||||||
|
expect(scene.entities.count).toBe(initialCount - 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('世代版本特性验证', () => {
|
||||||
|
test('回收的ID应该不能被误用', () => {
|
||||||
|
const entity = scene.createEntity('GenerationTest');
|
||||||
|
const oldId = entity.id;
|
||||||
|
|
||||||
|
// 销毁实体
|
||||||
|
entity.destroy();
|
||||||
|
|
||||||
|
// 等待延迟回收处理
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.advanceTimersByTime(150);
|
||||||
|
|
||||||
|
// 创建新实体触发回收处理
|
||||||
|
const newEntity = scene.createEntity('NewEntity');
|
||||||
|
|
||||||
|
// 旧ID应该无效
|
||||||
|
expect(scene.identifierPool.isValid(oldId)).toBe(false);
|
||||||
|
|
||||||
|
// 新ID应该有效
|
||||||
|
expect(scene.identifierPool.isValid(newEntity.id)).toBe(true);
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ID验证应该工作正常', () => {
|
||||||
|
const entity = scene.createEntity('ValidateTest');
|
||||||
|
const validId = entity.id;
|
||||||
|
const invalidId = 999999;
|
||||||
|
|
||||||
|
expect(scene.identifierPool.isValid(validId)).toBe(true);
|
||||||
|
expect(scene.identifierPool.isValid(invalidId)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
72
tests/setup.ts
Normal file
72
tests/setup.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
2
thirdparty/BehaviourTree-ai
vendored
2
thirdparty/BehaviourTree-ai
vendored
Submodule thirdparty/BehaviourTree-ai updated: 3ee55de0f3...98aba30ec1
2
thirdparty/mvvm-ui-framework
vendored
2
thirdparty/mvvm-ui-framework
vendored
Submodule thirdparty/mvvm-ui-framework updated: 9c28abce28...e7044b9a7a
46
tsconfig.test.json
Normal file
46
tsconfig.test.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": false,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"exactOptionalPropertyTypes": false,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"noUncheckedIndexedAccess": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"importHelpers": false,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"isolatedModules": false,
|
||||||
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"tests/**/*.ts",
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"bin",
|
||||||
|
"dist",
|
||||||
|
"examples",
|
||||||
|
"thirdparty"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user