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
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* The tables a running game looks its graphs up in: they answer as searching the graph does, and an
|
||||
* edit to the project reaches the run once the run is told about it.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { AddPortsCommand } from '../../src/core/commands/portCommands';
|
||||
import { searchingIndex, tabledIndex } from '../../src/core/graph/graphIndex';
|
||||
import { graphLookup } from '../../src/core/model/graphLookup';
|
||||
import { graphEditor, makeBehavior, makeSpriteScene, place, startWorld } from '../helpers';
|
||||
|
||||
describe('the tabled index', () => {
|
||||
it('answers as searching the graph does, edge order included', () => {
|
||||
const f = makeSpriteScene();
|
||||
const behavior = makeBehavior(f, 'B', ['n']);
|
||||
const edit = graphEditor(f, behavior);
|
||||
const update = edit.addNode('OnUpdate');
|
||||
const first = edit.addNode('Log', { message: 'first' });
|
||||
const second = edit.addNode('Log', { message: 'second' });
|
||||
const read = edit.variables(['n']);
|
||||
edit.wire([
|
||||
[update, 'exec', first, 'exec'],
|
||||
[update, 'exec', second, 'exec'],
|
||||
[first, 'exec', second, 'exec'],
|
||||
[read, 'n', second, 'message'],
|
||||
]);
|
||||
const lookup = graphLookup(f.project);
|
||||
const graph = behavior.graph;
|
||||
const searched = searchingIndex(graph, lookup);
|
||||
const tabled = tabledIndex(graph, lookup);
|
||||
for (const node of graph.nodes) {
|
||||
expect(tabled.node(node.id)).toBe(node);
|
||||
expect(tabled.def(node)).toEqual(searched.def(node));
|
||||
for (const port of ['exec', 'message', 'n', 'nothing']) {
|
||||
expect(tabled.into(node.id, port)).toEqual(searched.into(node.id, port));
|
||||
expect(tabled.outOf(node.id, port)).toEqual(searched.outOf(node.id, port));
|
||||
}
|
||||
}
|
||||
expect(tabled.outOf(update.id, 'exec').map((e) => e.to.node)).toEqual([first.id, second.id]);
|
||||
expect(tabled.into(second.id, 'exec')).toHaveLength(2);
|
||||
expect(tabled.ofType('Log')).toEqual([first, second]);
|
||||
expect(tabled.ofType('OnStart')).toEqual([]);
|
||||
expect(tabled.node('gone')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('a running game', () => {
|
||||
it('follows an edit to a graph from the frame after the run is told', () => {
|
||||
const f = makeSpriteScene();
|
||||
const behavior = makeBehavior(f, 'B', ['n']);
|
||||
place(f, behavior.id, { x: 0, y: 0 });
|
||||
const edit = graphEditor(f, behavior);
|
||||
const update = edit.addNode('OnUpdate');
|
||||
const log = edit.addNode('Log', { message: 'before' });
|
||||
edit.connect(update, 'exec', log, 'exec');
|
||||
const read = edit.variables([]);
|
||||
const { world, frame, behaviors } = startWorld(f, { behaviors: true });
|
||||
frame();
|
||||
expect(world.log).toEqual(['before']);
|
||||
|
||||
// A new edge, and a port the Variables node did not have when the run first looked at it.
|
||||
f.stack.execute(new AddPortsCommand(behavior.id, read.id, [{ key: 'n', kind: 'number' }]));
|
||||
edit.connect(read, 'n', log, 'message');
|
||||
edit.connect(update, 'exec', edit.addNode('Log', { message: 'added' }), 'exec');
|
||||
behaviors!.syncFromModel();
|
||||
world.log.length = 0;
|
||||
frame();
|
||||
expect(world.log).toEqual(['0', 'added']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user