Files
ComfyUI_frontend/tests-ui/tests/performance/spatialIndexPerformance.test.ts
Christian Byrne 316e05b8b9 [refactor] Reorganize Vue nodes to domain-driven design architecture (#5085)
* refactor: Reorganize Vue nodes system to domain-driven design architecture

Move Vue nodes code from scattered technical layers to domain-focused structure:

- Widget system → src/renderer/extensions/vueNodes/widgets/
- LOD optimization → src/renderer/extensions/vueNodes/lod/
- Layout logic → src/renderer/extensions/vueNodes/layout/
- Node components → src/renderer/extensions/vueNodes/components/
- Test structure mirrors source organization

Benefits:
- Clear domain boundaries instead of technical layers
- Everything Vue nodes related in renderer domain (not workbench)
- camelCase naming (vueNodes vs vue-nodes)
- Tests co-located with source domains
- All imports updated to new DDD structure

* fix: Skip spatial index performance test on CI to avoid flaky timing

Performance tests are inherently flaky on CI due to variable system
performance. This test should only run locally like the other
performance tests.
2025-08-18 16:58:45 -07:00

407 lines
13 KiB
TypeScript

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
})
})
})