Skip to content

Commit cf19759

Browse files
authored
Merge pull request #258 from pathsim/perf/routing-optimization
routing optimization
2 parents caa8eb2 + 1d5e471 commit cf19759

5 files changed

Lines changed: 291 additions & 123 deletions

File tree

src/lib/components/FlowCanvas.svelte

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,17 @@
5454
colorMode = theme;
5555
});
5656
57-
57+
// Debounced routing context update — coalesces rapid connection changes
58+
// (e.g. paste, undo, bulk delete) into a single recalculation
59+
let routingContextTimer: ReturnType<typeof setTimeout> | null = null;
60+
function scheduleRoutingUpdate() {
61+
if (routingContextTimer !== null) clearTimeout(routingContextTimer);
62+
routingContextTimer = setTimeout(() => {
63+
routingContextTimer = null;
64+
updateRoutingContext();
65+
}, 0);
66+
}
67+
5868
// Track mouse position for waypoint placement
5969
let mousePosition = { x: 0, y: 0 };
6070
@@ -238,7 +248,10 @@
238248
239249
// Cleanup function - will add subscriptions as they're defined
240250
const cleanups: (() => void)[] = [unsubscribeTheme, unsubscribeNodeUpdates, unsubscribeClearSelection, unsubscribeNudge, unsubscribeSelectNode];
241-
onDestroy(() => cleanups.forEach(fn => fn()));
251+
onDestroy(() => {
252+
cleanups.forEach(fn => fn());
253+
if (routingContextTimer !== null) clearTimeout(routingContextTimer);
254+
});
242255
243256
function clearPendingUpdates() {
244257
pendingNodeUpdates = [];
@@ -248,7 +261,7 @@
248261
// Returns handle tip position (accounting for handle offset from block edge)
249262
// For inputs, also accounts for arrowhead so stub starts within arrow
250263
function getPortInfo(nodeId: string, portIndex: number, isOutput: boolean): PortInfo | null {
251-
const node = nodes.find(n => n.id === nodeId);
264+
const node = nodeMap.get(nodeId);
252265
if (!node) return null;
253266
254267
const nodeData = node.data as NodeInstance;
@@ -382,6 +395,8 @@
382395
// Combined nodes for SvelteFlow
383396
let nodes = $state<Node[]>([]);
384397
let edges = $state<Edge[]>([]);
398+
// O(1) node lookup map — kept in sync with nodes array via $effect
399+
let nodeMap = $derived(new Map(nodes.map(n => [n.id, n])));
385400
386401
// Merge block, event, and annotation nodes when any changes
387402
// Preserve position and selection from SvelteFlow's current state (except during undo/redo)
@@ -640,8 +655,8 @@
640655
return edge;
641656
});
642657
// Recalculate routes when connections change
643-
// Use setTimeout to ensure nodes are updated first
644-
setTimeout(() => updateRoutingContext(), 0);
658+
// Debounced to coalesce rapid changes (paste, undo, bulk operations)
659+
scheduleRoutingUpdate();
645660
}));
646661
647662
// Track last snapped positions during drag for discrete routing updates

src/lib/routing/gridBuilder.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/**
22
* Sparse grid for pathfinding - stores only obstacles, computes walkability on demand
33
* Supports incremental updates for efficient node dragging
4+
*
5+
* Performance: spatial hash uses numeric bucket keys to avoid string GC
6+
* in the hot isWalkableAt path (called thousands of times per A* run).
47
*/
58

69
import type { RoutingContext, Bounds, PortStub } from './types';
@@ -10,6 +13,14 @@ import { GRID_SIZE, ROUTING_MARGIN } from './constants';
1013
/** Size of spatial hash buckets (in grid cells) */
1114
const SPATIAL_BUCKET_SIZE = 10;
1215

16+
/**
17+
* Encode bucket coordinates into a single number.
18+
* Bucket coords are small (typically -50..+50), so 10_000 offset is plenty.
19+
*/
20+
function encodeBucket(bx: number, by: number): number {
21+
return (bx + 10_000) * 20_001 + (by + 10_000);
22+
}
23+
1324
/**
1425
* Convert world coordinates to grid coordinates
1526
* Since everything is grid-aligned, this is a simple division
@@ -57,7 +68,7 @@ function boundsToObstacle(bounds: Bounds, offsetX: number, offsetY: number): Gri
5768

5869
/**
5970
* Sparse grid that computes walkability on-demand from obstacle list
60-
* No matrix storage - O(obstacles) memory instead of O(width × height)
71+
* No matrix storage - O(obstacles) memory instead of O(width x height)
6172
* Supports incremental updates - O(1) to update a single node
6273
* Effectively unbounded - only obstacles block movement
6374
*/
@@ -71,33 +82,33 @@ export class SparseGrid {
7182
/** Port stub obstacles (rebuilt when stubs change) */
7283
private portStubObstacles: GridObstacle[] = [];
7384

74-
/** Spatial hash: bucket key -> set of node IDs with obstacles in that bucket */
75-
private spatialHash: Map<string, Set<string>> = new Map();
85+
/** Spatial hash: numeric bucket key -> set of node IDs with obstacles in that bucket */
86+
private spatialHash: Map<number, Set<string>> = new Map();
7687

7788
constructor(context?: RoutingContext) {
7889
if (context) {
7990
this.initFromContext(context);
8091
}
8192
}
8293

83-
/** Get bucket key for a grid coordinate */
84-
private getBucketKey(gx: number, gy: number): string {
94+
/** Get numeric bucket key for a grid coordinate */
95+
private getBucketKey(gx: number, gy: number): number {
8596
const bx = Math.floor(gx / SPATIAL_BUCKET_SIZE);
8697
const by = Math.floor(gy / SPATIAL_BUCKET_SIZE);
87-
return `${bx},${by}`;
98+
return encodeBucket(bx, by);
8899
}
89100

90101
/** Get all bucket keys that an obstacle overlaps */
91-
private getObstacleBuckets(obs: GridObstacle): string[] {
92-
const keys: string[] = [];
102+
private getObstacleBuckets(obs: GridObstacle): number[] {
103+
const keys: number[] = [];
93104
const minBx = Math.floor(obs.minGx / SPATIAL_BUCKET_SIZE);
94105
const maxBx = Math.floor(obs.maxGx / SPATIAL_BUCKET_SIZE);
95106
const minBy = Math.floor(obs.minGy / SPATIAL_BUCKET_SIZE);
96107
const maxBy = Math.floor(obs.maxGy / SPATIAL_BUCKET_SIZE);
97108

98109
for (let bx = minBx; bx <= maxBx; bx++) {
99110
for (let by = minBy; by <= maxBy; by++) {
100-
keys.push(`${bx},${by}`);
111+
keys.push(encodeBucket(bx, by));
101112
}
102113
}
103114
return keys;

0 commit comments

Comments
 (0)