mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-30 21:09:53 +00:00
* 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.
407 lines
13 KiB
TypeScript
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
|
|
})
|
|
})
|
|
})
|