Skip to content

Commit 1db58fb

Browse files
fix: resolve severe performance issues in synapse, cortex, and brain pages
Synapse graph was creating/destroying hundreds of PixiJS objects on every d3-force tick (~600 ticks). Now pre-runs simulation synchronously and draws once. Brain canvas Bloom reduced and antialias disabled. Cluster canvas stops re-rendering on inspector state changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 46ba158 commit 1db58fb

3 files changed

Lines changed: 88 additions & 81 deletions

File tree

dashboard/src/components/brain/brain-canvas.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ export function BrainCanvas({ scrollProgress }: { scrollProgress: number }) {
177177
return (
178178
<Canvas
179179
camera={{ position: [0, 0, 8], fov: 50 }}
180-
gl={{ antialias: true, alpha: true }}
180+
gl={{ antialias: false, alpha: true, powerPreference: "high-performance" }}
181+
dpr={[1, 1.5]}
181182
style={{ background: NEURAL.void }}
182183
frameloop="always"
183184
>
@@ -202,12 +203,11 @@ export function BrainCanvas({ scrollProgress }: { scrollProgress: number }) {
202203
minPolarAngle={Math.PI * 0.35}
203204
/>
204205

205-
<EffectComposer>
206+
<EffectComposer multisampling={0}>
206207
<Bloom
207-
intensity={1.5}
208-
luminanceThreshold={0.2}
209-
luminanceSmoothing={0.9}
210-
mipmapBlur
208+
intensity={0.8}
209+
luminanceThreshold={0.3}
210+
luminanceSmoothing={0.5}
211211
/>
212212
</EffectComposer>
213213
</Suspense>

dashboard/src/components/clusters/cluster-canvas.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@ export function ClusterCanvas() {
1414
const appRef = useRef<Application | null>(null);
1515
const { data } = useMemories({ limit: 500 });
1616
const { dimension, transitioning } = useClusterStore();
17-
const openInspector = useInspectorStore((s) => s.open);
17+
const openInspectorRef = useRef(useInspectorStore.getState().open);
18+
19+
// Keep ref in sync without causing redraws
20+
useEffect(() => {
21+
return useInspectorStore.subscribe((s) => {
22+
openInspectorRef.current = s.open;
23+
});
24+
}, []);
1825

1926
const memories = useMemo(() => data?.memories ?? [], [data]);
2027

@@ -33,8 +40,8 @@ export function ClusterCanvas() {
3340
canvas,
3441
resizeTo: canvas.parentElement || undefined,
3542
backgroundColor: 0x050510,
36-
antialias: true,
37-
resolution: window.devicePixelRatio || 1,
43+
antialias: false,
44+
resolution: Math.min(window.devicePixelRatio || 1, 2),
3845
autoDensity: true,
3946
});
4047
appRef.current = app;
@@ -130,15 +137,15 @@ export function ClusterCanvas() {
130137

131138
gfx.on("pointerdown", (e: FederatedPointerEvent) => {
132139
e.stopPropagation();
133-
openInspector(memory.id);
140+
openInspectorRef.current(memory.id);
134141
});
135142

136143
gfx.on("pointerover", () => gfx.scale.set(1.5));
137144
gfx.on("pointerout", () => gfx.scale.set(1));
138145

139146
nodesContainer.addChild(gfx);
140147
}
141-
}, [memories, dimension, openInspector]);
148+
}, [memories, dimension]);
142149

143150
useEffect(() => {
144151
draw();

dashboard/src/components/synapses/synapse-graph.tsx

Lines changed: 70 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ export function SynapseGraph() {
4242
canvas,
4343
resizeTo: canvas.parentElement || undefined,
4444
backgroundColor: 0x050510,
45-
antialias: true,
46-
resolution: window.devicePixelRatio || 1,
45+
antialias: false,
46+
resolution: Math.min(window.devicePixelRatio || 1, 2),
4747
autoDensity: true,
4848
});
4949
appRef.current = app;
@@ -61,100 +61,100 @@ export function SynapseGraph() {
6161
return;
6262
}
6363

64-
const edgesContainer = new Container();
65-
const nodesContainer = new Container();
66-
app.stage.addChild(edgesContainer);
67-
app.stage.addChild(nodesContainer);
68-
69-
// Run d3-force
64+
// Build force nodes
7065
const forceNodes: ForceNode[] = nodes.map(n => ({
7166
id: n.id,
7267
layer: n.layer,
7368
strength: n.strength,
7469
label: n.label || '',
7570
}));
7671

77-
const forceEdges = edges.map(e => ({
72+
const forceEdges: SimulationLinkDatum<ForceNode>[] = edges.map(e => ({
7873
source: e.source,
7974
target: e.target,
8075
type: e.type,
81-
}));
76+
})) as SimulationLinkDatum<ForceNode>[];
8277

78+
// Pre-run simulation to completion synchronously — no per-tick rendering
8379
const sim = forceSimulation<ForceNode>(forceNodes)
8480
.force(
8581
"link",
86-
forceLink<ForceNode, SimulationLinkDatum<ForceNode>>(forceEdges as SimulationLinkDatum<ForceNode>[])
82+
forceLink<ForceNode, SimulationLinkDatum<ForceNode>>(forceEdges)
8783
.id(d => d.id)
8884
.distance(80)
8985
.strength(0.2)
9086
)
9187
.force("charge", forceManyBody().strength(-100))
9288
.force("center", forceCenter(canvas.clientWidth / 2, canvas.clientHeight / 2))
93-
.force("collide", forceCollide(15));
89+
.force("collide", forceCollide(15))
90+
.stop(); // Don't auto-run
91+
92+
// Run 300 ticks synchronously (no rendering in between)
93+
for (let i = 0; i < 300; i++) {
94+
sim.tick();
95+
}
96+
97+
// Build node lookup map for O(1) edge resolution
98+
const nodeMap = new Map(forceNodes.map(n => [n.id, n]));
99+
100+
// Draw once — edges
101+
const edgesContainer = new Container();
102+
const nodesContainer = new Container();
103+
app.stage.addChild(edgesContainer);
104+
app.stage.addChild(nodesContainer);
94105

95-
sim.on("tick", () => {
96-
edgesContainer.removeChildren();
97-
nodesContainer.removeChildren();
106+
const edgeGfx = new Graphics();
107+
for (const edge of forceEdges) {
108+
const sourceNode = edge.source as unknown as ForceNode;
109+
const targetNode = edge.target as unknown as ForceNode;
110+
if (!sourceNode.x || !sourceNode.y || !targetNode.x || !targetNode.y) continue;
98111

99-
// Draw edges as synapses
100-
const edgeGfx = new Graphics();
101-
for (const edge of forceEdges) {
102-
const s = forceNodes.find(n => n.id === (edge.source as unknown as ForceNode).id || n.id === edge.source);
103-
const t = forceNodes.find(n => n.id === (edge.target as unknown as ForceNode).id || n.id === edge.target);
104-
if (!s?.x || !s?.y || !t?.x || !t?.y) continue;
112+
const color = (edge as { type?: string }).type === "category" ? 0x7c3aed : 0x22d3ee;
113+
edgeGfx.moveTo(sourceNode.x, sourceNode.y);
114+
edgeGfx.lineTo(targetNode.x, targetNode.y);
115+
edgeGfx.stroke({ width: 1, color, alpha: 0.12 });
116+
}
117+
edgesContainer.addChild(edgeGfx);
118+
119+
// Draw once — nodes
120+
for (const node of forceNodes) {
121+
if (!node.x || !node.y) continue;
122+
123+
const radius = 4 + node.strength * 8;
124+
const colorHex = node.layer === "sml" ? 0x22d3ee : 0xfbbf24;
125+
126+
const gfx = new Graphics();
127+
// Glow
128+
gfx.circle(0, 0, radius * 2.5);
129+
gfx.fill({ color: colorHex, alpha: 0.05 });
130+
// Node
131+
gfx.circle(0, 0, radius);
132+
gfx.fill({ color: colorHex, alpha: 0.8 });
133+
134+
gfx.position.set(node.x, node.y);
135+
gfx.eventMode = "static";
136+
gfx.cursor = "pointer";
137+
138+
gfx.on("pointerdown", (e: FederatedPointerEvent) => {
139+
e.stopPropagation();
140+
openInspector(node.id);
141+
});
142+
gfx.on("pointerover", () => gfx.scale.set(1.4));
143+
gfx.on("pointerout", () => gfx.scale.set(1));
105144

106-
const color = edge.type === "category" ? 0x7c3aed : 0x22d3ee;
107-
const alpha = 0.12;
145+
nodesContainer.addChild(gfx);
108146

109-
edgeGfx.moveTo(s.x, s.y);
110-
edgeGfx.lineTo(t.x, t.y);
111-
edgeGfx.stroke({ width: 1, color, alpha });
112-
}
113-
edgesContainer.addChild(edgeGfx);
114-
115-
// Draw nodes
116-
for (const node of forceNodes) {
117-
if (!node.x || !node.y) continue;
118-
119-
const radius = 4 + node.strength * 8;
120-
const colorHex = node.layer === "sml" ? 0x22d3ee : 0xfbbf24;
121-
122-
const gfx = new Graphics();
123-
// Glow
124-
gfx.circle(0, 0, radius * 2.5);
125-
gfx.fill({ color: colorHex, alpha: 0.05 });
126-
// Node
127-
gfx.circle(0, 0, radius);
128-
gfx.fill({ color: colorHex, alpha: 0.8 });
129-
130-
gfx.position.set(node.x, node.y);
131-
gfx.eventMode = "static";
132-
gfx.cursor = "pointer";
133-
134-
gfx.on("pointerdown", (e: FederatedPointerEvent) => {
135-
e.stopPropagation();
136-
openInspector(node.id);
147+
// Label only for strong nodes
148+
if (node.strength > 0.5 && node.label) {
149+
const label = new Text({
150+
text: node.label.slice(0, 25),
151+
style: new TextStyle({ fontSize: 8, fill: 0x94a3b8, fontFamily: "system-ui" }),
137152
});
138-
gfx.on("pointerover", () => gfx.scale.set(1.4));
139-
gfx.on("pointerout", () => gfx.scale.set(1));
140-
141-
nodesContainer.addChild(gfx);
142-
143-
// Label
144-
if (node.strength > 0.5 && node.label) {
145-
const label = new Text({
146-
text: node.label.slice(0, 25),
147-
style: new TextStyle({ fontSize: 8, fill: 0x94a3b8, fontFamily: "system-ui" }),
148-
});
149-
label.anchor.set(0.5, 0);
150-
label.position.set(node.x, node.y + radius + 4);
151-
nodesContainer.addChild(label);
152-
}
153+
label.anchor.set(0.5, 0);
154+
label.position.set(node.x, node.y + radius + 4);
155+
nodesContainer.addChild(label);
153156
}
154-
});
155-
156-
// Stop after 200 ticks
157-
sim.alpha(1).alphaDecay(0.005);
157+
}
158158
}, [data, openInspector]);
159159

160160
useEffect(() => {

0 commit comments

Comments
 (0)