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>
70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
/**
|
|
* 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']);
|
|
});
|
|
});
|