Files
feuerwerk/tests/core/graph-index.test.ts
T
adminandClaude Opus 5.5 a7e793f6de 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>
2026-09-29 10:36:10 +02:00

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']);
});
});