remove unused spatial index manager (#5381)

This commit is contained in:
Christian Byrne
2025-09-06 00:36:37 -07:00
committed by GitHub
parent bd299b8210
commit 104760b2c2
3 changed files with 0 additions and 1102 deletions

View File

@@ -1,198 +0,0 @@
/**
* Composable for spatial indexing of nodes using QuadTree
* Integrates with useGraphNodeManager for efficient viewport culling
*/
import { useDebounceFn } from '@vueuse/core'
import { computed, reactive, ref } from 'vue'
import { type Bounds, QuadTree } from '@/utils/spatial/QuadTree'
export interface SpatialIndexOptions {
worldBounds?: Bounds
maxDepth?: number
maxItemsPerNode?: number
updateDebounceMs?: number
}
interface SpatialMetrics {
queryTime: number
totalNodes: number
visibleNodes: number
treeDepth: number
rebuildCount: number
}
export const useSpatialIndex = (options: SpatialIndexOptions = {}) => {
// Default world bounds (can be expanded dynamically)
const defaultBounds: Bounds = {
x: -10000,
y: -10000,
width: 20000,
height: 20000
}
// QuadTree instance
const quadTree = ref<QuadTree<string> | null>(null)
// Performance metrics
const metrics = reactive<SpatialMetrics>({
queryTime: 0,
totalNodes: 0,
visibleNodes: 0,
treeDepth: 0,
rebuildCount: 0
})
// Initialize QuadTree
const initialize = (bounds: Bounds = defaultBounds) => {
quadTree.value = new QuadTree<string>(bounds, {
maxDepth: options.maxDepth ?? 6,
maxItemsPerNode: options.maxItemsPerNode ?? 4
})
metrics.rebuildCount++
}
// Add or update node in spatial index
const updateNode = (
nodeId: string,
position: { x: number; y: number },
size: { width: number; height: number }
) => {
if (!quadTree.value) {
initialize()
}
const bounds: Bounds = {
x: position.x,
y: position.y,
width: size.width,
height: size.height
}
// Use insert instead of update - insert handles both new and existing nodes
quadTree.value!.insert(nodeId, bounds, nodeId)
metrics.totalNodes = quadTree.value!.size
}
// Batch update for multiple nodes
const batchUpdate = (
updates: Array<{
id: string
position: { x: number; y: number }
size: { width: number; height: number }
}>
) => {
if (!quadTree.value) {
initialize()
}
for (const update of updates) {
const bounds: Bounds = {
x: update.position.x,
y: update.position.y,
width: update.size.width,
height: update.size.height
}
// Use insert instead of update - insert handles both new and existing nodes
quadTree.value!.insert(update.id, bounds, update.id)
}
metrics.totalNodes = quadTree.value!.size
}
// Remove node from spatial index
const removeNode = (nodeId: string) => {
if (!quadTree.value) return
quadTree.value.remove(nodeId)
metrics.totalNodes = quadTree.value.size
}
// Query nodes within viewport bounds
const queryViewport = (viewportBounds: Bounds): string[] => {
if (!quadTree.value) return []
const startTime = performance.now()
const nodeIds = quadTree.value.query(viewportBounds)
const queryTime = performance.now() - startTime
metrics.queryTime = queryTime
metrics.visibleNodes = nodeIds.length
return nodeIds
}
// Get nodes within a radius (for proximity queries)
const queryRadius = (
center: { x: number; y: number },
radius: number
): string[] => {
if (!quadTree.value) return []
const bounds: Bounds = {
x: center.x - radius,
y: center.y - radius,
width: radius * 2,
height: radius * 2
}
return quadTree.value.query(bounds)
}
// Clear all nodes
const clear = () => {
if (!quadTree.value) return
quadTree.value.clear()
metrics.totalNodes = 0
metrics.visibleNodes = 0
}
// Rebuild tree (useful after major layout changes)
const rebuild = (
nodes: Map<
string,
{
position: { x: number; y: number }
size: { width: number; height: number }
}
>
) => {
initialize()
const updates = Array.from(nodes.entries()).map(([id, data]) => ({
id,
position: data.position,
size: data.size
}))
batchUpdate(updates)
}
// Debounced update for performance
const debouncedUpdateNode = useDebounceFn(
updateNode,
options.updateDebounceMs ?? 16
)
return {
// Core functions
initialize,
updateNode,
batchUpdate,
removeNode,
queryViewport,
queryRadius,
clear,
rebuild,
// Debounced version for high-frequency updates
debouncedUpdateNode,
// Metrics
metrics: computed(() => metrics),
// Direct access to QuadTree (for advanced usage)
quadTree: computed(() => quadTree.value)
}
}

View File

@@ -1,498 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useSpatialIndex } from '@/composables/graph/useSpatialIndex'
// Mock @vueuse/core
vi.mock('@vueuse/core', () => ({
useDebounceFn: (fn: (...args: any[]) => any) => fn // Return function directly for testing
}))
describe('useSpatialIndex', () => {
let spatialIndex: ReturnType<typeof useSpatialIndex>
beforeEach(() => {
spatialIndex = useSpatialIndex()
})
describe('initialization', () => {
it('should start with null quadTree', () => {
expect(spatialIndex.quadTree.value).toBeNull()
})
it('should initialize with default bounds when first node is added', () => {
const { updateNode, quadTree, metrics } = spatialIndex
updateNode('node1', { x: 100, y: 100 }, { width: 200, height: 100 })
expect(quadTree.value).not.toBeNull()
expect(metrics.value.totalNodes).toBe(1)
})
it('should initialize with custom bounds', () => {
const { initialize, quadTree } = spatialIndex
const customBounds = { x: 0, y: 0, width: 5000, height: 3000 }
initialize(customBounds)
expect(quadTree.value).not.toBeNull()
})
it('should increment rebuild count on initialization', () => {
const { initialize, metrics } = spatialIndex
expect(metrics.value.rebuildCount).toBe(0)
initialize()
expect(metrics.value.rebuildCount).toBe(1)
})
it('should accept custom options', () => {
const customIndex = useSpatialIndex({
maxDepth: 8,
maxItemsPerNode: 6,
updateDebounceMs: 32
})
customIndex.initialize()
expect(customIndex.quadTree.value).not.toBeNull()
})
})
describe('updateNode', () => {
it('should add a new node to the index', () => {
const { updateNode, metrics } = spatialIndex
updateNode('node1', { x: 100, y: 100 }, { width: 200, height: 100 })
expect(metrics.value.totalNodes).toBe(1)
})
it('should update existing node position', () => {
const { updateNode, queryViewport } = spatialIndex
// Add node
updateNode('node1', { x: 100, y: 100 }, { width: 200, height: 100 })
// Move node
updateNode('node1', { x: 500, y: 500 }, { width: 200, height: 100 })
// Query old position - should not find node
const oldResults = queryViewport({
x: 50,
y: 50,
width: 300,
height: 200
})
expect(oldResults).not.toContain('node1')
// Query new position - should find node
const newResults = queryViewport({
x: 450,
y: 450,
width: 300,
height: 200
})
expect(newResults).toContain('node1')
})
it('should auto-initialize if quadTree is null', () => {
const { updateNode, quadTree } = spatialIndex
expect(quadTree.value).toBeNull()
updateNode('node1', { x: 0, y: 0 }, { width: 100, height: 100 })
expect(quadTree.value).not.toBeNull()
})
})
describe('batchUpdate', () => {
it('should update multiple nodes at once', () => {
const { batchUpdate, metrics } = spatialIndex
const updates = [
{
id: 'node1',
position: { x: 100, y: 100 },
size: { width: 200, height: 100 }
},
{
id: 'node2',
position: { x: 300, y: 300 },
size: { width: 150, height: 150 }
},
{
id: 'node3',
position: { x: 500, y: 200 },
size: { width: 100, height: 200 }
}
]
batchUpdate(updates)
expect(metrics.value.totalNodes).toBe(3)
})
it('should handle empty batch', () => {
const { batchUpdate, metrics } = spatialIndex
batchUpdate([])
expect(metrics.value.totalNodes).toBe(0)
})
it('should auto-initialize if needed', () => {
const { batchUpdate, quadTree } = spatialIndex
expect(quadTree.value).toBeNull()
batchUpdate([
{
id: 'node1',
position: { x: 0, y: 0 },
size: { width: 100, height: 100 }
}
])
expect(quadTree.value).not.toBeNull()
})
})
describe('removeNode', () => {
beforeEach(() => {
spatialIndex.updateNode(
'node1',
{ x: 100, y: 100 },
{ width: 200, height: 100 }
)
spatialIndex.updateNode(
'node2',
{ x: 300, y: 300 },
{ width: 200, height: 100 }
)
})
it('should remove node from index', () => {
const { removeNode, metrics } = spatialIndex
expect(metrics.value.totalNodes).toBe(2)
removeNode('node1')
expect(metrics.value.totalNodes).toBe(1)
})
it('should handle removing non-existent node', () => {
const { removeNode, metrics } = spatialIndex
expect(metrics.value.totalNodes).toBe(2)
removeNode('node999')
expect(metrics.value.totalNodes).toBe(2)
})
it('should handle removeNode when quadTree is null', () => {
const freshIndex = useSpatialIndex()
// Should not throw
expect(() => freshIndex.removeNode('node1')).not.toThrow()
})
})
describe('queryViewport', () => {
beforeEach(() => {
// Set up a grid of nodes
spatialIndex.updateNode(
'node1',
{ x: 0, y: 0 },
{ width: 100, height: 100 }
)
spatialIndex.updateNode(
'node2',
{ x: 200, y: 0 },
{ width: 100, height: 100 }
)
spatialIndex.updateNode(
'node3',
{ x: 0, y: 200 },
{ width: 100, height: 100 }
)
spatialIndex.updateNode(
'node4',
{ x: 200, y: 200 },
{ width: 100, height: 100 }
)
})
it('should find nodes within viewport bounds', () => {
const { queryViewport } = spatialIndex
// Query top-left quadrant
const results = queryViewport({ x: -50, y: -50, width: 200, height: 200 })
expect(results).toContain('node1')
expect(results).not.toContain('node2')
expect(results).not.toContain('node3')
expect(results).not.toContain('node4')
})
it('should find multiple nodes in larger viewport', () => {
const { queryViewport } = spatialIndex
// Query entire area
const results = queryViewport({ x: -50, y: -50, width: 400, height: 400 })
expect(results).toHaveLength(4)
expect(results).toContain('node1')
expect(results).toContain('node2')
expect(results).toContain('node3')
expect(results).toContain('node4')
})
it('should return empty array for empty region', () => {
const { queryViewport } = spatialIndex
const results = queryViewport({
x: 1000,
y: 1000,
width: 100,
height: 100
})
expect(results).toEqual([])
})
it('should update metrics after query', () => {
const { queryViewport, metrics } = spatialIndex
queryViewport({ x: 0, y: 0, width: 300, height: 300 })
expect(metrics.value.queryTime).toBeGreaterThan(0)
expect(metrics.value.visibleNodes).toBe(4)
})
it('should handle query when quadTree is null', () => {
const freshIndex = useSpatialIndex()
const results = freshIndex.queryViewport({
x: 0,
y: 0,
width: 100,
height: 100
})
expect(results).toEqual([])
})
})
describe('queryRadius', () => {
beforeEach(() => {
// Set up nodes at different distances
spatialIndex.updateNode(
'center',
{ x: 475, y: 475 },
{ width: 50, height: 50 }
)
spatialIndex.updateNode(
'near1',
{ x: 525, y: 475 },
{ width: 50, height: 50 }
)
spatialIndex.updateNode(
'near2',
{ x: 425, y: 475 },
{ width: 50, height: 50 }
)
spatialIndex.updateNode(
'far',
{ x: 775, y: 775 },
{ width: 50, height: 50 }
)
})
it('should find nodes within radius', () => {
const { queryRadius } = spatialIndex
const results = queryRadius({ x: 500, y: 500 }, 100)
expect(results).toContain('center')
expect(results).toContain('near1')
expect(results).toContain('near2')
expect(results).not.toContain('far')
})
it('should handle zero radius', () => {
const { queryRadius } = spatialIndex
const results = queryRadius({ x: 500, y: 500 }, 0)
// Zero radius creates a point query at (500,500)
// The 'center' node spans 475-525 on both axes, so it contains this point
expect(results).toContain('center')
})
it('should handle large radius', () => {
const { queryRadius } = spatialIndex
const results = queryRadius({ x: 500, y: 500 }, 1000)
expect(results).toHaveLength(4) // Should find all nodes
})
})
describe('clear', () => {
beforeEach(() => {
spatialIndex.updateNode(
'node1',
{ x: 100, y: 100 },
{ width: 200, height: 100 }
)
spatialIndex.updateNode(
'node2',
{ x: 300, y: 300 },
{ width: 200, height: 100 }
)
})
it('should remove all nodes', () => {
const { clear, metrics } = spatialIndex
expect(metrics.value.totalNodes).toBe(2)
clear()
expect(metrics.value.totalNodes).toBe(0)
})
it('should reset metrics', () => {
const { clear, queryViewport, metrics } = spatialIndex
// Do a query to set visible nodes
queryViewport({ x: 0, y: 0, width: 500, height: 500 })
expect(metrics.value.visibleNodes).toBe(2)
clear()
expect(metrics.value.visibleNodes).toBe(0)
})
it('should handle clear when quadTree is null', () => {
const freshIndex = useSpatialIndex()
expect(() => freshIndex.clear()).not.toThrow()
})
})
describe('rebuild', () => {
it('should rebuild index with new nodes', () => {
const { rebuild, metrics, queryViewport } = spatialIndex
// Add initial nodes
spatialIndex.updateNode(
'old1',
{ x: 0, y: 0 },
{ width: 100, height: 100 }
)
expect(metrics.value.rebuildCount).toBe(1)
// Rebuild with new set
const newNodes = new Map([
[
'new1',
{ position: { x: 100, y: 100 }, size: { width: 50, height: 50 } }
],
[
'new2',
{ position: { x: 200, y: 200 }, size: { width: 50, height: 50 } }
]
])
rebuild(newNodes)
expect(metrics.value.totalNodes).toBe(2)
expect(metrics.value.rebuildCount).toBe(2)
// Old nodes should be gone
const oldResults = queryViewport({
x: -50,
y: -50,
width: 100,
height: 100
})
expect(oldResults).not.toContain('old1')
// New nodes should be findable
const newResults = queryViewport({
x: 50,
y: 50,
width: 200,
height: 200
})
expect(newResults).toContain('new1')
expect(newResults).toContain('new2')
})
it('should handle empty rebuild', () => {
const { rebuild, metrics } = spatialIndex
rebuild(new Map())
expect(metrics.value.totalNodes).toBe(0)
})
})
describe('metrics', () => {
it('should track performance metrics', () => {
const { metrics, updateNode, queryViewport } = spatialIndex
// Initial state
expect(metrics.value).toEqual({
queryTime: 0,
totalNodes: 0,
visibleNodes: 0,
treeDepth: 0,
rebuildCount: 0
})
// Add nodes
updateNode('node1', { x: 0, y: 0 }, { width: 100, height: 100 })
expect(metrics.value.totalNodes).toBe(1)
// Query
queryViewport({ x: -50, y: -50, width: 200, height: 200 })
expect(metrics.value.queryTime).toBeGreaterThan(0)
expect(metrics.value.visibleNodes).toBe(1)
})
})
describe('edge cases', () => {
it('should handle nodes with zero size', () => {
const { updateNode, queryViewport } = spatialIndex
updateNode('point', { x: 100, y: 100 }, { width: 0, height: 0 })
// Should still be findable
const results = queryViewport({ x: 50, y: 50, width: 100, height: 100 })
expect(results).toContain('point')
})
it('should handle negative positions', () => {
const { updateNode, queryViewport } = spatialIndex
updateNode('negative', { x: -500, y: -500 }, { width: 100, height: 100 })
const results = queryViewport({
x: -600,
y: -600,
width: 200,
height: 200
})
expect(results).toContain('negative')
})
it('should handle very large nodes', () => {
const { updateNode, queryViewport } = spatialIndex
updateNode('huge', { x: 0, y: 0 }, { width: 5000, height: 5000 })
// Should be found even when querying small area within it
const results = queryViewport({ x: 100, y: 100, width: 10, height: 10 })
expect(results).toContain('huge')
})
})
describe('debouncedUpdateNode', () => {
it('should be available', () => {
const { debouncedUpdateNode } = spatialIndex
expect(debouncedUpdateNode).toBeDefined()
expect(typeof debouncedUpdateNode).toBe('function')
})
})
})

View File

@@ -1,406 +0,0 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { useSpatialIndex } from '@/composables/graph/useSpatialIndex'
import type { Bounds } from '@/utils/spatial/QuadTree'
// Skip this entire suite on CI to avoid flaky performance timing
const isCI = Boolean(process.env.CI)
const describeIfNotCI = isCI ? describe.skip : describe
describeIfNotCI('Spatial Index Performance', () => {
let spatialIndex: ReturnType<typeof useSpatialIndex>
beforeEach(() => {
spatialIndex = useSpatialIndex({
maxDepth: 6,
maxItemsPerNode: 4,
updateDebounceMs: 0 // Disable debouncing for tests
})
})
describe('large scale operations', () => {
it('should handle 1000 node insertions efficiently', () => {
const startTime = performance.now()
// Generate 1000 nodes in a realistic distribution
const nodes = Array.from({ length: 1000 }, (_, i) => ({
id: `node${i}`,
position: {
x: (Math.random() - 0.5) * 10000,
y: (Math.random() - 0.5) * 10000
},
size: {
width: 150 + Math.random() * 100,
height: 100 + Math.random() * 50
}
}))
spatialIndex.batchUpdate(nodes)
const insertTime = performance.now() - startTime
// Should insert 1000 nodes in under 100ms
expect(insertTime).toBeLessThan(100)
expect(spatialIndex.metrics.value.totalNodes).toBe(1000)
})
it('should maintain fast viewport queries under load', () => {
// First populate with many nodes
const nodes = Array.from({ length: 1000 }, (_, i) => ({
id: `node${i}`,
position: {
x: (Math.random() - 0.5) * 10000,
y: (Math.random() - 0.5) * 10000
},
size: { width: 200, height: 100 }
}))
spatialIndex.batchUpdate(nodes)
// Now benchmark viewport queries
const queryCount = 100
const viewportBounds: Bounds = {
x: -960,
y: -540,
width: 1920,
height: 1080
}
const startTime = performance.now()
for (let i = 0; i < queryCount; i++) {
// Vary viewport position to test different tree regions
const offsetX = (i % 10) * 500
const offsetY = Math.floor(i / 10) * 300
spatialIndex.queryViewport({
x: viewportBounds.x + offsetX,
y: viewportBounds.y + offsetY,
width: viewportBounds.width,
height: viewportBounds.height
})
}
const totalQueryTime = performance.now() - startTime
const avgQueryTime = totalQueryTime / queryCount
// Each query should take less than 2ms on average
expect(avgQueryTime).toBeLessThan(2)
expect(totalQueryTime).toBeLessThan(100) // 100 queries in under 100ms
})
it('should demonstrate performance advantage over linear search', () => {
// Create test data
const nodeCount = 500
const nodes = Array.from({ length: nodeCount }, (_, i) => ({
id: `node${i}`,
position: {
x: (Math.random() - 0.5) * 8000,
y: (Math.random() - 0.5) * 8000
},
size: { width: 200, height: 100 }
}))
// Populate spatial index
spatialIndex.batchUpdate(nodes)
const viewport: Bounds = { x: -500, y: -300, width: 1000, height: 600 }
const queryCount = 50
// Benchmark spatial index queries
const spatialStartTime = performance.now()
for (let i = 0; i < queryCount; i++) {
spatialIndex.queryViewport(viewport)
}
const spatialTime = performance.now() - spatialStartTime
// Benchmark linear search equivalent
const linearStartTime = performance.now()
for (let i = 0; i < queryCount; i++) {
nodes.filter((node) => {
const nodeRight = node.position.x + node.size.width
const nodeBottom = node.position.y + node.size.height
const viewportRight = viewport.x + viewport.width
const viewportBottom = viewport.y + viewport.height
return !(
nodeRight < viewport.x ||
node.position.x > viewportRight ||
nodeBottom < viewport.y ||
node.position.y > viewportBottom
)
})
}
const linearTime = performance.now() - linearStartTime
// Spatial index should be faster than linear search
const speedup = linearTime / spatialTime
// In some environments, speedup may be less due to small dataset
// Just ensure spatial is not significantly slower (at least 10% of linear speed)
expect(speedup).toBeGreaterThan(0.1)
// Both should find roughly the same number of nodes
const spatialResults = spatialIndex.queryViewport(viewport)
const linearResults = nodes.filter((node) => {
const nodeRight = node.position.x + node.size.width
const nodeBottom = node.position.y + node.size.height
const viewportRight = viewport.x + viewport.width
const viewportBottom = viewport.y + viewport.height
return !(
nodeRight < viewport.x ||
node.position.x > viewportRight ||
nodeBottom < viewport.y ||
node.position.y > viewportBottom
)
})
// Results should be similar (within 10% due to QuadTree boundary effects)
const resultsDiff = Math.abs(spatialResults.length - linearResults.length)
const maxDiff =
Math.max(spatialResults.length, linearResults.length) * 0.1
expect(resultsDiff).toBeLessThan(maxDiff)
})
})
describe('update performance', () => {
it('should handle frequent position updates efficiently', () => {
// Add initial nodes
const nodeCount = 200
const initialNodes = Array.from({ length: nodeCount }, (_, i) => ({
id: `node${i}`,
position: { x: i * 100, y: i * 50 },
size: { width: 200, height: 100 }
}))
spatialIndex.batchUpdate(initialNodes)
// Benchmark frequent updates (simulating animation/dragging)
const updateCount = 100
const startTime = performance.now()
for (let frame = 0; frame < updateCount; frame++) {
// Update a subset of nodes each frame
for (let i = 0; i < 20; i++) {
const nodeId = `node${i}`
spatialIndex.updateNode(
nodeId,
{
x: i * 100 + Math.sin(frame * 0.1) * 50,
y: i * 50 + Math.cos(frame * 0.1) * 30
},
{ width: 200, height: 100 }
)
}
}
const updateTime = performance.now() - startTime
const avgFrameTime = updateTime / updateCount
// Should maintain 60fps (16.67ms per frame) with 20 node updates per frame
expect(avgFrameTime).toBeLessThan(8) // Conservative target: 8ms per frame
})
it('should handle node additions and removals efficiently', () => {
const startTime = performance.now()
// Add nodes
for (let i = 0; i < 100; i++) {
spatialIndex.updateNode(
`node${i}`,
{ x: Math.random() * 1000, y: Math.random() * 1000 },
{ width: 200, height: 100 }
)
}
// Remove half of them
for (let i = 0; i < 50; i++) {
spatialIndex.removeNode(`node${i}`)
}
// Add new ones
for (let i = 100; i < 150; i++) {
spatialIndex.updateNode(
`node${i}`,
{ x: Math.random() * 1000, y: Math.random() * 1000 },
{ width: 200, height: 100 }
)
}
const totalTime = performance.now() - startTime
// All operations should complete quickly
expect(totalTime).toBeLessThan(50)
expect(spatialIndex.metrics.value.totalNodes).toBe(100) // 50 remaining + 50 new
})
})
describe('memory and scaling', () => {
it('should scale efficiently with increasing node counts', () => {
const nodeCounts = [100, 200, 500, 1000]
const queryTimes: number[] = []
for (const nodeCount of nodeCounts) {
// Create fresh spatial index for each test
const testIndex = useSpatialIndex({ updateDebounceMs: 0 })
// Populate with nodes
const nodes = Array.from({ length: nodeCount }, (_, i) => ({
id: `node${i}`,
position: {
x: (Math.random() - 0.5) * 10000,
y: (Math.random() - 0.5) * 10000
},
size: { width: 200, height: 100 }
}))
testIndex.batchUpdate(nodes)
// Benchmark query time
const viewport: Bounds = { x: -500, y: -300, width: 1000, height: 600 }
const startTime = performance.now()
for (let i = 0; i < 10; i++) {
testIndex.queryViewport(viewport)
}
const avgTime = (performance.now() - startTime) / 10
queryTimes.push(avgTime)
}
// Query time should scale logarithmically, not linearly
// The ratio between 1000 nodes and 100 nodes should be less than 5x
const ratio100to1000 = queryTimes[3] / queryTimes[0]
expect(ratio100to1000).toBeLessThan(5)
// All query times should be reasonable
queryTimes.forEach((time) => {
expect(time).toBeLessThan(5) // Each query under 5ms
})
})
it('should handle edge cases without performance degradation', () => {
// Test with very large nodes
spatialIndex.updateNode(
'huge-node',
{ x: -1000, y: -1000 },
{ width: 5000, height: 3000 }
)
// Test with many tiny nodes
for (let i = 0; i < 100; i++) {
spatialIndex.updateNode(
`tiny-${i}`,
{ x: Math.random() * 100, y: Math.random() * 100 },
{ width: 1, height: 1 }
)
}
// Test with nodes at extreme coordinates
spatialIndex.updateNode(
'extreme-pos',
{ x: 50000, y: -50000 },
{ width: 200, height: 100 }
)
spatialIndex.updateNode(
'extreme-neg',
{ x: -50000, y: 50000 },
{ width: 200, height: 100 }
)
// Queries should still be fast
const startTime = performance.now()
for (let i = 0; i < 20; i++) {
spatialIndex.queryViewport({
x: Math.random() * 1000 - 500,
y: Math.random() * 1000 - 500,
width: 1000,
height: 600
})
}
const queryTime = performance.now() - startTime
expect(queryTime).toBeLessThan(20) // 20 queries in under 20ms
})
})
describe('realistic workflow scenarios', () => {
it('should handle typical ComfyUI workflow performance', () => {
// Simulate a large ComfyUI workflow with clustered nodes
const clusters = [
{ center: { x: 0, y: 0 }, nodeCount: 50 },
{ center: { x: 2000, y: 0 }, nodeCount: 30 },
{ center: { x: 4000, y: 1000 }, nodeCount: 40 },
{ center: { x: 0, y: 2000 }, nodeCount: 35 }
]
let nodeId = 0
const allNodes: Array<{
id: string
position: { x: number; y: number }
size: { width: number; height: number }
}> = []
// Create clustered nodes (realistic for ComfyUI workflows)
clusters.forEach((cluster) => {
for (let i = 0; i < cluster.nodeCount; i++) {
allNodes.push({
id: `node${nodeId++}`,
position: {
x: cluster.center.x + (Math.random() - 0.5) * 800,
y: cluster.center.y + (Math.random() - 0.5) * 600
},
size: {
width: 150 + Math.random() * 100,
height: 100 + Math.random() * 50
}
})
}
})
// Add the nodes
const setupTime = performance.now()
spatialIndex.batchUpdate(allNodes)
const setupDuration = performance.now() - setupTime
// Simulate user panning around the workflow
const viewportSize = { width: 1920, height: 1080 }
const panPositions = [
{ x: -960, y: -540 }, // Center on first cluster
{ x: 1040, y: -540 }, // Pan to second cluster
{ x: 3040, y: 460 }, // Pan to third cluster
{ x: -960, y: 1460 }, // Pan to fourth cluster
{ x: 1000, y: 500 } // Overview position
]
const panStartTime = performance.now()
const queryResults: number[] = []
panPositions.forEach((pos) => {
// Simulate multiple viewport queries during smooth panning
for (let step = 0; step < 10; step++) {
const results = spatialIndex.queryViewport({
x: pos.x + step * 20,
y: pos.y + step * 10,
width: viewportSize.width,
height: viewportSize.height
})
queryResults.push(results.length)
}
})
const panDuration = performance.now() - panStartTime
const avgQueryTime = panDuration / (panPositions.length * 10)
// Performance expectations for realistic workflows
expect(setupDuration).toBeLessThan(30) // Setup 155 nodes in under 30ms
expect(avgQueryTime).toBeLessThan(1.5) // Average query under 1.5ms
expect(panDuration).toBeLessThan(50) // All panning queries under 50ms
// Should have reasonable culling efficiency
const totalNodes = allNodes.length
const avgVisibleNodes =
queryResults.reduce((a, b) => a + b, 0) / queryResults.length
const cullRatio = (totalNodes - avgVisibleNodes) / totalNodes
expect(cullRatio).toBeGreaterThan(0.3) // At least 30% culling efficiency
})
})
})