test(02-02): add full-pipeline pathfinding performance test and record review-fix round

- 新增 performance.test.ts:100x100 固定种子障碍图上计时完整 find 流水线
  (建图 + 最优路径发现),结构化输出 map/steps/elapsed
- 实测 map=100x100 steps=198 elapsed≈609ms(sanity 上限 5000ms)
- 02-02-SUMMARY 追加 Review Fixes (user audit round 1) 记录
This commit is contained in:
unanmed 2026-09-09 18:51:31 +08:00
parent d9ee80f77f
commit 489e3de106
2 changed files with 332 additions and 0 deletions

View File

@ -238,3 +238,41 @@ None - no external service configuration required.
- 验证命令复跑合规补丁后mover.test.ts 4/4 + path 目录 18/18 全绿eslint 目标目录 0 problems`check:type` 过滤 `src[\\/]path[\\/]` 无输出;`check:circular` 无 path 相关路径(既有基线循环不变)
- requirements.ready-ids0/2 readyPATH-01/PATH-02 与 02-03 共享shared-ID 门生效frontmatter `requirements-completed: []`
- broken-windows 台账02-01 的 skipped-test #4 标记 fixed新增 #5interrupt 占位 stub, open
## Review Fixes (user audit round 1)
> 用户对 Wave 2 产出的代码审查修复轮9 条指令全部落实);根因已由用户在 `9a60442` 自行修复(`useMover(mover: IObjectMover<IObjectMovable> | null)`),本轮对齐实现侧。
### 性能测试结果(完整 find 流水线:建图 + 最优路径发现)
```text
[pathfinding-perf] map=100x100 steps=198 elapsed=608.55ms
```
- 地图100 x 10010000 节点),固定种子生成约 20% 墙体的确定性障碍图案,起点 (0,0) 至终点 (99,99)
- 最优步数198恰为 99 + 99 的曼哈顿距离,绕障未增加步数)
- 总耗时608.55 ms`performance.now()` 计时,宽松 sanity 上限 5000 ms`pnpm exec vitest run` 单测 619ms 完成全流程
### 指令落实明细
| # | 指令 | 落实情况 | 验证 |
| - | ---- | ---- | ---- |
| 1 | 禁止 `'mover' in xxx` 运行时形状检查 | 删除 `IMovableWithMover` 接口与 `hasMover` 结构化守卫,`PathfindingSystem` 直接持有 `IObjectMover<IObjectMovable> \| null` 类型绑定,位置经 `mover.tile.x/y` 读取path/*.ts 复扫 0 残留 | grep `'mover' in` 无命中23/23 测试绿 |
| 2 | 类与接口本身不带 doc 注释 | 移除 `PathfindingGraphBuilder`/`PathfindingFinder`/`PathfindingSystem` 类注释与 `IPathfindingGraphBuilder` 接口注释(类型迁入 types.ts 时同样不写接口注释);测试侧移除纯复述的 `TestTileDefinition`/`TestTile`/`SystemFixture` 注释 | 逐文件核查;例外:测试 `FixturePredicate` 保留(「复刻 DefaultHeroMoveTopImpl 掩码语义」为来源说明dev.md 特殊情况例外) |
| 3 | 实现侧不重复接口已有的注释 | 类实现侧与接口逐字重复的方法 jsdoc 全部移除useMapState/useMapLayer/useCostFunction/usePassPredicate/useDirGroup/useFallbackPolicy/useMover/moveTo/teleportTo/interrupt/build仅保留内容确不相同者`find`(告警+空数组契约、不可达语义)与 `getPath`(未绑定告警语义) | 类型门 0 诊断 |
| 4 | 导出类型归位 types.ts | `IPathGraphEdge`/`IPathGraphNode`/`IPathGraph`/`IPathfindingGraphBuilder` 迁入 path/types.tsgraph.ts/finder.ts 改由 `./types` 导入;未改动用户既有接口语义 | `pnpm check:type` 过滤 `src[\\/]path[\\/]` 无输出 |
| 5 | 私有方法先于调用者 | `resolveFloorId` 移至 `build` 前;`getNodeCost`→`search`→`find` 链式前置;`startMove` 移至 `moveTo`/`teleportTo` 前 | 代码走查 |
| 6 | 短文件不用 region | 移除 graph.ts/finder.ts/system.ts 全部 `#region`/`#endregion`(移动器识别/寻路系统等 5 处) | grep 无命中 |
| 7 | 注释面向 API 使用者而非维护者 | `search` jsdoc 去除「Dijkstra」算法叙述`getNodeCost` 去除「非负权不变式」内部不变量叙述;`interrupt` 去除「03 计划接线」计划性叙述(其余与接口重复故整条移除);保留的 jsdoc 均为可观察行为契约 | 代码走查 |
| 8 | 对齐新 types.ts`useMover` | `useMover` 参数改收 `IObjectMover<IObjectMovable> \| null`,字段 `movable`→`mover`,测试改传 `tile.mover`测试描述同步movable→moverpath/*.ts 无 `movable` 残留标识符 | grep `movable` 无命中check:type 0 诊断 |
| 9 | 性能测试 | 新建 `path/performance.test.ts`100x100 固定种子障碍图,计时 `system.getPath()`(绑定系统上的完整建图+搜索流水线),结构化输出 map/steps/elapsed断言找到路径且耗时低于 5000ms sanity 上限,常驻通过不 skip | 见上方性能结果 |
### 验证汇总(修复轮全部复核)
- `pnpm exec vitest run "packages-user/data-system/src/path"` — 4 文件 23 passed含新增性能测试
- `pnpm exec vitest run "packages-user/data-common/src/common/mover.test.ts"` — 4/4 绿
- `pnpm check:type` 过滤 `src[\\/]path[\\/]|common[\\/]mover` — 0 诊断(其余 legacy 既有诊断不在本计划范围)
- `pnpm check:circular` — 无涉及 path 的循环(`common/mover.ts` 所在循环节点全部为本轮未触碰的既有基线边)
- `pnpm exec eslint` 目标目录path/ + data-common mover 两文件)— 0 problems
**提交:** `d9ee80f`refactor指令 18、本提交test指令 9 + 本记录)

View File

@ -0,0 +1,294 @@
// 测试寻路性能:从有向图构建到最优路径发现的完整 find 流水线总耗时
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type ITileLocator } from '@motajs/common';
import { FaceDirection } from '@user/data-common';
import {
type IDataCommon,
type IFaceHandler,
type IObjectMovable,
type IObjectMover,
type ITileStore
} from '@user/data-common';
import {
type IGameMap,
type IPassCheckHandler,
type IPassPredicate
} from '@user/data-base';
import { type PathfindingSystem } from './system';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
Map.prototype.getOrInsertComputed ??= function <K, V>(
this: Map<K, V>,
key: K,
callback: (key: K) => V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
const value = callback(key);
this.set(key, value);
return value;
};
});
interface TestModules {
PathfindingSystem: typeof import('./system').PathfindingSystem;
MapState: typeof import('@user/data-base').MapState;
TileStore: typeof import('@user/data-common').TileStore;
ObjectMover: typeof import('@user/data-common').ObjectMover;
FaceManager: typeof import('@user/data-common').FaceManager;
Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler;
RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const systemModule = await import('./system');
const baseModule = await import('@user/data-base');
const commonModule = await import('@user/data-common');
modules = {
PathfindingSystem: systemModule.PathfindingSystem,
MapState: baseModule.MapState,
TileStore: commonModule.TileStore,
ObjectMover: commonModule.ObjectMover,
FaceManager: commonModule.FaceManager,
Dir8FaceHandler: commonModule.Dir8FaceHandler,
RoleFaceBinder: commonModule.RoleFaceBinder
};
});
/** 性能地图边长,共 100 x 100 = 10000 个节点 */
const MAP_SIZE = 100;
/** 性能测试的宽松耗时上限,单位 ms仅作 sanity 把关 */
const ELAPSED_LIMIT_MS = 5000;
interface TestTile extends IObjectMovable {
x: number;
y: number;
face: FaceDirection;
mover: IObjectMover<TestTile>;
}
/**
* DefaultHeroMoveTopImpl
*
*/
class FixturePredicate implements IPassPredicate {
/** 绑定的楼层地图对象 */
private readonly map: IGameMap;
/** 朝向管理对象,用于求相反方向 */
private readonly face: IFaceHandler<FaceDirection>;
constructor(map: IGameMap, face: IFaceHandler<FaceDirection>) {
this.map = map;
this.face = face;
}
/**
*
* @param dir
*/
private passBit(dir: FaceDirection): number {
switch (dir) {
case FaceDirection.Up:
return 0b0001;
case FaceDirection.Right:
return 0b0010;
case FaceDirection.Down:
return 0b0100;
case FaceDirection.Left:
return 0b1000;
default:
return 0;
}
}
canPass(handler: IPassCheckHandler): boolean {
const event = this.map.eventLayer;
if (!event) return false;
const { currLoc, nextLoc, direction } = handler;
// 四角朝向直接判定为可通行,与 moverImpl 语义一致
if (
direction === FaceDirection.LeftDown ||
direction === FaceDirection.LeftUp ||
direction === FaceDirection.RightDown ||
direction === FaceDirection.RightUp
) {
return true;
}
const opposite = this.face.opposite(direction);
const leaveMask = this.passBit(direction);
const enterMask = this.passBit(opposite);
let canLeave = true;
let canEnter = true;
const curr = event.getLocationData(currLoc.x, currLoc.y);
const next = event.getLocationData(nextLoc.x, nextLoc.y);
const currRaw = curr?.static.raw();
const nextRaw = next?.static.raw();
if (currRaw) {
canLeave = !!(leaveMask & currRaw.pass.outPass);
}
if (nextRaw) {
canEnter = !!(enterMask & nextRaw.pass.inPass);
}
return canLeave && canEnter;
}
shouldHit(): boolean {
return false;
}
}
/**
*
*
* @param size
*/
function generateRows(size: number): number[] {
let seed = 20260909;
const next = () => {
seed = (seed * 1664525 + 1013904223) % 4294967296;
return seed / 4294967296;
};
const rows: number[] = [];
for (let i = 0; i < size * size; i++) {
rows.push(next() < 0.2 ? 6 : 1);
}
rows[0] = 1;
rows[size * size - 1] = 1;
return rows;
}
/**
* 100 x 100
* @param rows
* @param width
*/
function createPerformanceSystem(rows: number[], width: number) {
const tileStore: ITileStore = new modules.TileStore() as never;
const tileDefs = [
{ num: 1, id: 'open', outPass: 15, inPass: 15 },
{ num: 6, id: 'wall', outPass: 0, inPass: 0 }
];
for (const tile of tileDefs) {
tileStore.addTile({
num: tile.num,
id: tile.id,
events: {},
type: 0,
pass: {
onlyEvents: false,
outPass: tile.outPass,
inPass: tile.inPass
},
eventPass: true
});
}
const faceManager = new modules.FaceManager();
faceManager.register(1, new modules.Dir8FaceHandler());
const commonState: IDataCommon = {
tileStore,
itemStore: {},
mapStore: {},
eventStore: {},
roleFace: new modules.RoleFaceBinder(),
faceManager,
saveSystem: {}
} as never;
const maps = new modules.MapState(tileStore, commonState);
const map = maps.fromRaw({
floorId: 'PERF',
width,
map: { 0: rows },
layerAlias: { 0: 'event' },
events: { 0: {} }
})!;
const layer = map.getLayerByAlias('event')!;
const predicate = new FixturePredicate(map, new modules.Dir8FaceHandler());
class PerfMover
extends modules.ObjectMover<TestTile>
implements IObjectMover<TestTile>
{
readonly tile: TestTile;
constructor(tile: TestTile) {
super(new modules.Dir8FaceHandler(), FaceDirection.Down);
this.tile = tile;
}
protected override async onMoveStart(): Promise<void> {}
protected override async onMoveEnd(): Promise<void> {}
protected override async onStepStart(): Promise<number> {
return 0;
}
protected override async onStepEnd(): Promise<ITileLocator> {
return { x: this.tile.x, y: this.tile.y };
}
protected override async onStepSettled(): Promise<void> {}
}
class PerfTile implements TestTile {
x: number = 0;
y: number = 0;
face: FaceDirection = FaceDirection.Down;
mover: IObjectMover<TestTile>;
constructor() {
this.mover = new PerfMover(this);
}
setPos(x: number, y: number): void {
this.x = x;
this.y = y;
}
getCurrentFaceDirection(): FaceDirection {
return this.face;
}
}
const tile = new PerfTile();
const system: PathfindingSystem = new modules.PathfindingSystem(
commonState as never
);
system.useMover(tile.mover);
system.finder.useMapState(maps);
system.finder.useMapLayer(layer);
system.finder.usePassPredicate(predicate);
return system;
}
describe('pathfinding performance', () => {
// 验证 100 x 100 障碍地图上从建图到最优路径发现的完整寻路耗时处于可用量级
it('completes the full find pipeline on a 100x100 map within the sanity bound', () => {
const rows = generateRows(MAP_SIZE);
const system = createPerformanceSystem(rows, MAP_SIZE);
const start = performance.now();
const steps = system.getPath({ x: MAP_SIZE - 1, y: MAP_SIZE - 1 });
const elapsed = performance.now() - start;
// 审查要求输出结构化性能数据供汇报使用
// eslint-disable-next-line no-console
console.log(
`[pathfinding-perf] map=${MAP_SIZE}x${MAP_SIZE} ` +
`steps=${steps.length} elapsed=${elapsed.toFixed(2)}ms`
);
expect(steps.length).toBeGreaterThan(0);
expect(elapsed).toBeLessThan(ELAPSED_LIMIT_MS);
});
});