Skip to content

Commit 1701435

Browse files
committed
optimize sync hot path and add dedicated perf benchmark
注意不要破坏公开api和现有测试 建议先生成专门的性能测试以对比效果
1 parent 04ad736 commit 1701435

File tree

3 files changed

+168
-44
lines changed

3 files changed

+168
-44
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { component, relation, type EntityId } from "../core/entity";
3+
import { World } from "../core/world";
4+
5+
function benchmark(label: string, warmupRounds: number, measuredRounds: number, fn: (round: number) => void): number {
6+
const durations: number[] = [];
7+
8+
const totalRounds = warmupRounds + measuredRounds;
9+
for (let round = 0; round < totalRounds; round++) {
10+
const start = performance.now();
11+
fn(round);
12+
const duration = performance.now() - start;
13+
if (round >= warmupRounds) {
14+
durations.push(duration);
15+
}
16+
}
17+
18+
const average = durations.reduce((sum, duration) => sum + duration, 0) / durations.length;
19+
console.log(
20+
`${label}: avg ${average.toFixed(2)}ms after ${warmupRounds} warmup rounds (${durations
21+
.map((d) => d.toFixed(2))
22+
.join("ms, ")}ms per measured round)`,
23+
);
24+
return average;
25+
}
26+
27+
describe("World sync hot-path performance", () => {
28+
it("should keep stable sync throughput for frequent set/remove patterns", () => {
29+
const world = new World();
30+
const Position = component<{ x: number; y: number }>();
31+
const ChildOf = component({ dontFragment: true, exclusive: true });
32+
33+
const parentA = world.new();
34+
const parentB = world.new();
35+
36+
const entityCount = 4000;
37+
const entities: EntityId[] = [];
38+
39+
for (let i = 0; i < entityCount; i++) {
40+
const entity = world.new();
41+
entities.push(entity);
42+
world.set(entity, Position, { x: i, y: i });
43+
world.set(entity, relation(ChildOf, parentA));
44+
}
45+
world.sync();
46+
47+
const warmupRounds = 2;
48+
const measuredRounds = 8;
49+
50+
const positionAverage = benchmark("position update + sync", warmupRounds, measuredRounds, (round) => {
51+
for (let i = 0; i < entities.length; i++) {
52+
const entity = entities[i]!;
53+
world.set(entity, Position, { x: round, y: i });
54+
}
55+
world.sync();
56+
});
57+
58+
const relationAverage = benchmark(
59+
"exclusive dontFragment relation flip + sync",
60+
warmupRounds,
61+
measuredRounds,
62+
(round) => {
63+
const target = round % 2 === 0 ? parentB : parentA;
64+
for (let i = 0; i < entities.length; i++) {
65+
const entity = entities[i]!;
66+
world.set(entity, relation(ChildOf, target));
67+
}
68+
world.sync();
69+
},
70+
);
71+
72+
expect(world.query([Position]).length).toBe(entityCount);
73+
expect(world.query([relation(ChildOf, "*")]).length).toBe(entityCount);
74+
75+
// Guard against pathological regressions while keeping CI variance tolerance.
76+
expect(positionAverage).toBeLessThan(250);
77+
expect(relationAverage).toBeLessThan(350);
78+
});
79+
});

src/core/archetype.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
getWildcardRelationDataSource,
77
isRelationType,
88
} from "./archetype-helpers";
9-
import type { EntityId, WildcardRelationId } from "./entity";
109
import { normalizeComponentTypes } from "./component-type-utils";
10+
import type { EntityId, WildcardRelationId } from "./entity";
1111
import {
1212
getComponentIdFromRelationId,
1313
getDetailedIdType,
@@ -156,6 +156,10 @@ export class Archetype {
156156
return entityData;
157157
}
158158

159+
getEntityDontFragmentRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined {
160+
return this.dontFragmentRelations.get(entityId);
161+
}
162+
159163
dump(): Array<{ entity: EntityId; components: Map<EntityId<any>, any> }> {
160164
return this.entities.map((entity, i) => {
161165
const components = new Map<EntityId<any>, any>();

src/core/world-commands.ts

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,9 @@ export function removeMatchingRelations(
103103
}
104104

105105
// Check dontFragment relations stored on entity
106-
const entityData = archetype.getEntity(entityId);
107-
if (entityData) {
108-
for (const [componentType] of entityData) {
109-
if (archetype.componentTypeSet.has(componentType)) continue;
106+
const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
107+
if (dontFragmentData) {
108+
for (const componentType of dontFragmentData.keys()) {
110109
if (getComponentIdFromRelationId(componentType) === baseComponentId) {
111110
changeset.delete(componentType);
112111
}
@@ -140,14 +139,9 @@ export function maybeRemoveWildcardMarker(
140139
}
141140

142141
const wildcardMarker = relation(componentId, "*");
143-
const entityData = archetype.getEntity(entityId);
144-
if (!entityData) {
145-
changeset.delete(wildcardMarker);
146-
return;
147-
}
148142

149143
// Check if there are any other relations with the same component ID
150-
for (const [otherComponentType] of entityData) {
144+
for (const otherComponentType of archetype.componentTypes) {
151145
if (otherComponentType === removedComponentType) continue;
152146
if (otherComponentType === wildcardMarker) continue;
153147
if (changeset.removes.has(otherComponentType)) continue;
@@ -157,52 +151,99 @@ export function maybeRemoveWildcardMarker(
157151
}
158152
}
159153

154+
const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
155+
if (dontFragmentData) {
156+
for (const otherComponentType of dontFragmentData.keys()) {
157+
if (otherComponentType === removedComponentType) continue;
158+
if (changeset.removes.has(otherComponentType)) continue;
159+
160+
if (getComponentIdFromRelationId(otherComponentType) === componentId) {
161+
return; // Found another relation, keep the marker
162+
}
163+
}
164+
}
165+
160166
changeset.delete(wildcardMarker);
161167
}
162168

169+
function hasEntityComponent(archetype: Archetype, entityId: EntityId, componentType: EntityId<any>): boolean {
170+
if (archetype.componentTypeSet.has(componentType)) {
171+
return true;
172+
}
173+
174+
return archetype.getEntityDontFragmentRelations(entityId)?.has(componentType) ?? false;
175+
}
176+
177+
function pruneMissingRemovals(changeset: ComponentChangeset, archetype: Archetype, entityId: EntityId): void {
178+
for (const componentType of Array.from(changeset.removes)) {
179+
if (!hasEntityComponent(archetype, entityId, componentType)) {
180+
changeset.removes.delete(componentType);
181+
}
182+
}
183+
}
184+
185+
function hasArchetypeStructuralChange(changeset: ComponentChangeset, currentArchetype: Archetype): boolean {
186+
for (const componentType of changeset.removes) {
187+
if (!isDontFragmentRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) {
188+
return true;
189+
}
190+
}
191+
192+
for (const componentType of changeset.adds.keys()) {
193+
if (!isDontFragmentRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) {
194+
return true;
195+
}
196+
}
197+
198+
return false;
199+
}
200+
201+
function buildFinalRegularComponentTypes(currentArchetype: Archetype, changeset: ComponentChangeset): EntityId<any>[] {
202+
const finalRegularTypes = new Set(currentArchetype.componentTypes);
203+
204+
for (const componentType of changeset.removes) {
205+
if (!isDontFragmentRelation(componentType)) {
206+
finalRegularTypes.delete(componentType);
207+
}
208+
}
209+
210+
for (const componentType of changeset.adds.keys()) {
211+
if (!isDontFragmentRelation(componentType)) {
212+
finalRegularTypes.add(componentType);
213+
}
214+
}
215+
216+
return Array.from(finalRegularTypes);
217+
}
218+
163219
export function applyChangeset(
164220
ctx: CommandProcessorContext,
165221
entityId: EntityId,
166222
currentArchetype: Archetype,
167223
changeset: ComponentChangeset,
168224
entityToArchetype: Map<EntityId, Archetype>,
169225
): { removedComponents: Map<EntityId<any>, any>; newArchetype: Archetype } {
170-
const currentEntityData = currentArchetype.getEntity(entityId);
171-
const allCurrentComponentTypes = currentEntityData
172-
? Array.from(currentEntityData.keys())
173-
: currentArchetype.componentTypes;
174-
175-
const finalComponentTypes = changeset.getFinalComponentTypes(allCurrentComponentTypes);
176226
const removedComponents = new Map<EntityId<any>, any>();
177-
178-
if (finalComponentTypes) {
179-
// Check if archetype-affecting components actually changed
180-
// (dontFragment components don't affect archetype signature)
181-
const currentRegularTypes = filterRegularComponentTypes(allCurrentComponentTypes);
182-
const finalRegularTypes = filterRegularComponentTypes(finalComponentTypes);
183-
const archetypeChanged = !areComponentTypesEqual(currentRegularTypes, finalRegularTypes);
184-
185-
if (archetypeChanged) {
186-
// Move to new archetype (regular components changed)
187-
const newArchetype = moveEntityToNewArchetype(
188-
ctx,
189-
entityId,
190-
currentArchetype,
191-
finalComponentTypes,
192-
changeset,
193-
removedComponents,
194-
entityToArchetype,
195-
);
196-
return { removedComponents, newArchetype };
197-
} else {
198-
// Only dontFragment components changed, stay in same archetype
199-
updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
200-
}
201-
} else {
202-
// Update in same archetype
203-
updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
227+
pruneMissingRemovals(changeset, currentArchetype, entityId);
228+
const archetypeChanged = hasArchetypeStructuralChange(changeset, currentArchetype);
229+
230+
if (archetypeChanged) {
231+
const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
232+
const newArchetype = moveEntityToNewArchetype(
233+
ctx,
234+
entityId,
235+
currentArchetype,
236+
finalRegularTypes,
237+
changeset,
238+
removedComponents,
239+
entityToArchetype,
240+
);
241+
return { removedComponents, newArchetype };
204242
}
205243

244+
// No archetype move needed: only component payload updates and/or dontFragment relation updates.
245+
updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
246+
206247
return { removedComponents, newArchetype: currentArchetype };
207248
}
208249

0 commit comments

Comments
 (0)