Keep many spawned behaviors from eating the frame
A scene with 200 spawned gold pickups, each with a Pickup behavior of 97 nodes, dropped the game to about 14 fps. The graphs themselves ran only a dozen nodes per pickup and frame. The cost was the lookup: for every data input the interpreter searched all edges and all nodes of the graph linearly and computed the port shape of the node again. - New module core/graph/graphIndex.ts builds tables once per graph: nodes by id, edges by node and port, node types. Port shapes are cached as well. - BehaviorSystem shares one cache between all its instances and clears it in syncFromModel. A graph changed during a run applies from the next frame on, as before. - An interpreter without a cache, as in most tests, searches as before. Measured on the scene Performance test (headless per frame, browser fps on a throwaway copy of the project): - Headless: about 21 ms down to about 3.6 ms per frame. - Browser, about 505 entities: 13.6 fps up to 23 fps. - Browser, about 320 entities: 32.3 fps up to 48 fps. - After the fix behaviors take about 15 % of the browser profile. The rest is the editor mirroring the game into the model and rebuilding its panels, which is a separate change. Tests: - ./check.sh: 192 files, 2786 tests, CHECK EXIT 0. - Full e2e run (local): ALL PASSED, 1551 checks. - tests/core/graph-index.test.ts: 2 tests. The tables return the same as the search, in the same edge order. A graph changed during a run applies from the next frame. - Counter-check: without clearing the cache in syncFromModel the second test fails with ['before']. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5fbc6eb088
commit
a7e793f6de
4 files changed
+187
-16
No files matched your search
@@ -23,6 +23,7 @@ import {
|
||||
type TraceListener,
|
||||
type Value,
|
||||
} from '../../core/graph';
|
||||
import { GraphCache } from '../../core/graph/graphIndex';
|
||||
import { stepKeyRepeat } from '../../core/graph/keyRepeat';
|
||||
import { keyName } from '../../core/keys';
|
||||
import { graphLookup } from '../../core/model/graphLookup';
|
||||
@@ -194,6 +195,8 @@ export class BehaviorSystem implements System {
|
||||
private unranked = true;
|
||||
/** The declarations as last read; an edit is what differs from these (see `syncDeclarations`). */
|
||||
private declared: Record<string, PropertyBag> = {};
|
||||
/** Shared by every instance; an edit to the project empties it (see `syncFromModel`). */
|
||||
private readonly graphs = new GraphCache();
|
||||
private readonly collisions: CollisionSource | null;
|
||||
private readonly animations: AnimationSystem | null;
|
||||
private readonly audio: AudioSink;
|
||||
@@ -231,6 +234,7 @@ export class BehaviorSystem implements System {
|
||||
|
||||
/** An edit to the project while the run is on: declarations and the hierarchy may have changed. */
|
||||
syncFromModel(): void {
|
||||
this.graphs.clear();
|
||||
this.syncDeclarations();
|
||||
this.unranked = true;
|
||||
}
|
||||
@@ -323,7 +327,7 @@ export class BehaviorSystem implements System {
|
||||
if (!asset) continue;
|
||||
const trace = { edges: new Map<string, number>(), nodes: new Map<string, number>() };
|
||||
const values = new Map<Graph, Map<string, Produced>>();
|
||||
const interpreter = new GraphInterpreter(asset.graph, graphLookup(this.project));
|
||||
const interpreter = new GraphInterpreter(asset.graph, graphLookup(this.project), this.graphs);
|
||||
interpreter.trace = {
|
||||
onEdge: (id) => trace.edges.set(id, world.time),
|
||||
onNode: (id) => trace.nodes.set(id, world.time),
|
||||
|
||||
Reference in new issue
Block a user