mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-25 23:07:46 +00:00
Compare commits
1 Commits
pablo_hack
...
perf/subgr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4df5ec176e |
@@ -71,9 +71,9 @@
|
||||
@pointerup.capture="forwardPanEvent"
|
||||
@pointermove.capture="forwardPanEvent"
|
||||
>
|
||||
<!-- Vue nodes rendered based on graph nodes -->
|
||||
<!-- Vue nodes rendered progressively to avoid long-task UI freezes -->
|
||||
<LGraphNode
|
||||
v-for="nodeData in allNodes"
|
||||
v-for="nodeData in visibleNodes"
|
||||
:key="nodeData.id"
|
||||
:node-data="nodeData"
|
||||
:error="
|
||||
@@ -145,6 +145,7 @@ import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { installErrorClearingHooks } from '@/composables/graph/useErrorClearingHooks'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useProgressiveNodeRendering } from '@/composables/graph/useProgressiveNodeRendering'
|
||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||
import { useCanvasDrop } from '@/composables/useCanvasDrop'
|
||||
@@ -256,11 +257,23 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const allNodes = computed((): VueNodeData[] =>
|
||||
Array.from(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])
|
||||
)
|
||||
|
||||
const {
|
||||
visibleNodes,
|
||||
start: startProgressiveRender,
|
||||
reset: resetProgressiveRender
|
||||
} = useProgressiveNodeRendering(allNodes)
|
||||
|
||||
const handleVueNodeLifecycleReset = async () => {
|
||||
if (shouldRenderVueNodes.value) {
|
||||
resetProgressiveRender()
|
||||
vueNodeLifecycle.disposeNodeManagerAndSyncs()
|
||||
await nextTick()
|
||||
vueNodeLifecycle.initializeNodeManager()
|
||||
startProgressiveRender()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,10 +289,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const allNodes = computed((): VueNodeData[] =>
|
||||
Array.from(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])
|
||||
)
|
||||
|
||||
function onLinkOverlayReady(el: HTMLCanvasElement) {
|
||||
if (!canvasStore.canvas) return
|
||||
canvasStore.canvas.overlayCanvas = el
|
||||
|
||||
109
src/composables/graph/useProgressiveNodeRendering.test.ts
Normal file
109
src/composables/graph/useProgressiveNodeRendering.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useProgressiveNodeRendering } from '@/composables/graph/useProgressiveNodeRendering'
|
||||
|
||||
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
|
||||
layoutStore: {
|
||||
setPendingSlotSync: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
function makeNodes(count: number): VueNodeData[] {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: String(i)
|
||||
})) as VueNodeData[]
|
||||
}
|
||||
|
||||
describe('useProgressiveNodeRendering', () => {
|
||||
let rafCallbacks: Array<() => void>
|
||||
let originalRAF: typeof globalThis.requestAnimationFrame
|
||||
let originalCancel: typeof globalThis.cancelAnimationFrame
|
||||
|
||||
beforeEach(() => {
|
||||
rafCallbacks = []
|
||||
originalRAF = globalThis.requestAnimationFrame
|
||||
originalCancel = globalThis.cancelAnimationFrame
|
||||
|
||||
globalThis.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => {
|
||||
const id = rafCallbacks.length + 1
|
||||
rafCallbacks.push(() => cb(performance.now()))
|
||||
return id
|
||||
})
|
||||
globalThis.cancelAnimationFrame = vi.fn()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.requestAnimationFrame = originalRAF
|
||||
globalThis.cancelAnimationFrame = originalCancel
|
||||
})
|
||||
|
||||
it('renders all nodes immediately for small graphs', () => {
|
||||
const allNodes = ref(makeNodes(10))
|
||||
const { visibleNodes, start } = useProgressiveNodeRendering(allNodes)
|
||||
|
||||
start()
|
||||
|
||||
expect(visibleNodes.value).toHaveLength(10)
|
||||
expect(requestAnimationFrame).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders initial batch then progressively adds more', () => {
|
||||
const allNodes = ref(makeNodes(100))
|
||||
const { visibleNodes, start } = useProgressiveNodeRendering(allNodes)
|
||||
|
||||
start()
|
||||
|
||||
expect(visibleNodes.value.length).toBeLessThan(100)
|
||||
expect(visibleNodes.value.length).toBeGreaterThan(0)
|
||||
expect(requestAnimationFrame).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders all nodes after enough RAF frames', () => {
|
||||
const allNodes = ref(makeNodes(100))
|
||||
const { visibleNodes, start } = useProgressiveNodeRendering(allNodes)
|
||||
|
||||
start()
|
||||
|
||||
while (rafCallbacks.length > 0) {
|
||||
const cb = rafCallbacks.shift()!
|
||||
cb()
|
||||
}
|
||||
|
||||
expect(visibleNodes.value).toHaveLength(100)
|
||||
})
|
||||
|
||||
it('cancels in-flight rendering on reset', () => {
|
||||
const allNodes = ref(makeNodes(100))
|
||||
const { visibleNodes, start, reset } = useProgressiveNodeRendering(allNodes)
|
||||
|
||||
start()
|
||||
expect(visibleNodes.value.length).toBeGreaterThan(0)
|
||||
|
||||
reset()
|
||||
expect(visibleNodes.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles empty node list', () => {
|
||||
const allNodes = ref(makeNodes(0))
|
||||
const { visibleNodes, start } = useProgressiveNodeRendering(allNodes)
|
||||
|
||||
start()
|
||||
|
||||
expect(visibleNodes.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('tracks allNodes changes when not progressively rendering', async () => {
|
||||
const allNodes = ref(makeNodes(5))
|
||||
const { visibleNodes, start } = useProgressiveNodeRendering(allNodes)
|
||||
|
||||
start()
|
||||
expect(visibleNodes.value).toHaveLength(5)
|
||||
|
||||
allNodes.value = makeNodes(8)
|
||||
await nextTick()
|
||||
|
||||
expect(visibleNodes.value).toHaveLength(8)
|
||||
})
|
||||
})
|
||||
91
src/composables/graph/useProgressiveNodeRendering.ts
Normal file
91
src/composables/graph/useProgressiveNodeRendering.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
|
||||
const PROGRESSIVE_THRESHOLD = 40
|
||||
const INITIAL_BATCH_SIZE = 24
|
||||
const BATCH_SIZE = 40
|
||||
const FRAME_BUDGET_MS = 6
|
||||
|
||||
/**
|
||||
* Progressively renders nodes in batches across animation frames
|
||||
* to avoid blocking the main thread during subgraph transitions.
|
||||
*
|
||||
* For small graphs (≤ threshold), all nodes render immediately.
|
||||
* For larger graphs, an initial batch renders first, then remaining
|
||||
* nodes are added in RAF-driven batches with a per-frame time budget.
|
||||
*/
|
||||
export function useProgressiveNodeRendering(allNodes: Ref<VueNodeData[]>) {
|
||||
const renderedCount = ref(0)
|
||||
let renderToken = 0
|
||||
let rafId: number | null = null
|
||||
|
||||
const visibleNodes = computed(() =>
|
||||
allNodes.value.slice(0, renderedCount.value)
|
||||
)
|
||||
|
||||
function cancel() {
|
||||
renderToken++
|
||||
if (rafId != null) {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = null
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
cancel()
|
||||
renderedCount.value = 0
|
||||
}
|
||||
|
||||
function start() {
|
||||
cancel()
|
||||
|
||||
const total = allNodes.value.length
|
||||
if (total === 0) {
|
||||
renderedCount.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (total <= PROGRESSIVE_THRESHOLD) {
|
||||
renderedCount.value = total
|
||||
return
|
||||
}
|
||||
|
||||
layoutStore.setPendingSlotSync(true)
|
||||
const token = ++renderToken
|
||||
renderedCount.value = Math.min(INITIAL_BATCH_SIZE, total)
|
||||
|
||||
const pump = () => {
|
||||
if (token !== renderToken) return
|
||||
|
||||
const currentTotal = allNodes.value.length
|
||||
const start = performance.now()
|
||||
let next = renderedCount.value
|
||||
|
||||
while (next < currentTotal && performance.now() - start < FRAME_BUDGET_MS)
|
||||
next += BATCH_SIZE
|
||||
|
||||
renderedCount.value = Math.min(next, currentTotal)
|
||||
|
||||
if (renderedCount.value < currentTotal) {
|
||||
rafId = requestAnimationFrame(pump)
|
||||
} else {
|
||||
rafId = null
|
||||
}
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(pump)
|
||||
}
|
||||
|
||||
watch(allNodes, (nodes) => {
|
||||
if (rafId == null) {
|
||||
renderedCount.value = nodes.length
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(cancel)
|
||||
|
||||
return { visibleNodes, start, reset, cancel }
|
||||
}
|
||||
Reference in New Issue
Block a user