mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
perf: debounce convertToBlob in GLSL renderer
Use es-toolkit's debounce instead of hand-rolled implementation. Rapid calls within a 150ms window are coalesced into a single convertToBlob call. dispose() cancels pending conversions.
This commit is contained in:
@@ -1,18 +1,83 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { GLSLRendererConfig } from '@/renderer/glsl/useGLSLRenderer'
|
||||
import { useGLSLRenderer } from '@/renderer/glsl/useGLSLRenderer'
|
||||
|
||||
vi.mock('vue', async () => {
|
||||
const actual = await vi.importActual('vue')
|
||||
return {
|
||||
...actual,
|
||||
onScopeDispose: vi.fn()
|
||||
const mockBlob = new Blob(['test'], { type: 'image/jpeg' })
|
||||
|
||||
const mockConvertToBlob = vi.fn().mockResolvedValue(mockBlob)
|
||||
|
||||
// Stub OffscreenCanvas so init() succeeds in happy-dom
|
||||
vi.stubGlobal(
|
||||
'OffscreenCanvas',
|
||||
class {
|
||||
width: number
|
||||
height: number
|
||||
constructor(w: number, h: number) {
|
||||
this.width = w
|
||||
this.height = h
|
||||
}
|
||||
convertToBlob = mockConvertToBlob
|
||||
getContext() {
|
||||
return createMockGL()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
function createMockGL() {
|
||||
const noop = () => {}
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_target, prop) {
|
||||
if (prop === 'VERTEX_SHADER') return 35633
|
||||
if (prop === 'FRAGMENT_SHADER') return 35632
|
||||
if (prop === 'COMPILE_STATUS') return 35713
|
||||
if (prop === 'LINK_STATUS') return 35714
|
||||
if (prop === 'FRAMEBUFFER') return 36160
|
||||
if (prop === 'FRAMEBUFFER_COMPLETE') return 36053
|
||||
if (prop === 'COLOR_ATTACHMENT0') return 36064
|
||||
if (prop === 'TEXTURE_2D') return 3553
|
||||
if (prop === 'TEXTURE0') return 33984
|
||||
if (prop === 'RGBA') return 6408
|
||||
if (prop === 'RGBA8') return 32856
|
||||
if (prop === 'UNSIGNED_BYTE') return 5121
|
||||
if (prop === 'TEXTURE_MIN_FILTER') return 10241
|
||||
if (prop === 'TEXTURE_MAG_FILTER') return 10240
|
||||
if (prop === 'TEXTURE_WRAP_S') return 10242
|
||||
if (prop === 'TEXTURE_WRAP_T') return 10243
|
||||
if (prop === 'LINEAR') return 9729
|
||||
if (prop === 'CLAMP_TO_EDGE') return 33071
|
||||
if (prop === 'TRIANGLES') return 4
|
||||
if (prop === 'BACK') return 1029
|
||||
if (prop === 'UNPACK_FLIP_Y_WEBGL') return 37440
|
||||
if (prop === 'PACK_ROW_LENGTH') return 3330
|
||||
if (typeof prop === 'string' && prop.startsWith('get')) {
|
||||
return (..._args: unknown[]) => {
|
||||
if (prop === 'getShaderParameter') return true
|
||||
if (prop === 'getProgramParameter') return true
|
||||
if (prop === 'getExtension') return { loseContext: noop }
|
||||
if (prop === 'getUniformLocation') return 1
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof prop === 'string' &&
|
||||
(prop.startsWith('create') || prop === 'checkFramebufferStatus')
|
||||
) {
|
||||
return () => {
|
||||
if (prop === 'checkFramebufferStatus') return 36053
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return noop
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
describe('useGLSLRenderer', () => {
|
||||
it('returns renderer API with expected methods', async () => {
|
||||
const { useGLSLRenderer } = await import('@/renderer/glsl/useGLSLRenderer')
|
||||
it('returns renderer API with expected methods', () => {
|
||||
const renderer = useGLSLRenderer()
|
||||
|
||||
expect(renderer).toHaveProperty('init')
|
||||
@@ -27,27 +92,34 @@ describe('useGLSLRenderer', () => {
|
||||
expect(renderer).toHaveProperty('dispose')
|
||||
})
|
||||
|
||||
it('init returns false when WebGL2 is unavailable', async () => {
|
||||
const { useGLSLRenderer } = await import('@/renderer/glsl/useGLSLRenderer')
|
||||
it('init returns false when WebGL2 is unavailable', () => {
|
||||
const origOffscreenCanvas = globalThis.OffscreenCanvas
|
||||
vi.stubGlobal('OffscreenCanvas', undefined)
|
||||
const renderer = useGLSLRenderer()
|
||||
expect(renderer.init(256, 256)).toBe(false)
|
||||
vi.stubGlobal('OffscreenCanvas', origOffscreenCanvas)
|
||||
})
|
||||
|
||||
it('compileFragment reports error before initialization', async () => {
|
||||
const { useGLSLRenderer } = await import('@/renderer/glsl/useGLSLRenderer')
|
||||
it('compileFragment reports error before initialization', () => {
|
||||
const origOffscreenCanvas = globalThis.OffscreenCanvas
|
||||
vi.stubGlobal('OffscreenCanvas', undefined)
|
||||
const renderer = useGLSLRenderer()
|
||||
const result = renderer.compileFragment('void main() {}')
|
||||
expect(result.success).toBe(false)
|
||||
vi.stubGlobal('OffscreenCanvas', origOffscreenCanvas)
|
||||
})
|
||||
|
||||
it('toBlob rejects before initialization', async () => {
|
||||
const { useGLSLRenderer } = await import('@/renderer/glsl/useGLSLRenderer')
|
||||
const origOffscreenCanvas = globalThis.OffscreenCanvas
|
||||
vi.stubGlobal('OffscreenCanvas', undefined)
|
||||
const renderer = useGLSLRenderer()
|
||||
await expect(renderer.toBlob()).rejects.toThrow('Renderer not initialized')
|
||||
vi.stubGlobal('OffscreenCanvas', origOffscreenCanvas)
|
||||
})
|
||||
|
||||
it('accepts custom config without error', async () => {
|
||||
const { useGLSLRenderer } = await import('@/renderer/glsl/useGLSLRenderer')
|
||||
it('accepts custom config without error', () => {
|
||||
const origOffscreenCanvas = globalThis.OffscreenCanvas
|
||||
vi.stubGlobal('OffscreenCanvas', undefined)
|
||||
const config: GLSLRendererConfig = {
|
||||
maxInputs: 3,
|
||||
maxFloatUniforms: 2,
|
||||
@@ -57,5 +129,61 @@ describe('useGLSLRenderer', () => {
|
||||
}
|
||||
const renderer = useGLSLRenderer(config)
|
||||
expect(renderer.init(256, 256)).toBe(false)
|
||||
vi.stubGlobal('OffscreenCanvas', origOffscreenCanvas)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useGLSLRenderer debounced toBlob', () => {
|
||||
let renderer: ReturnType<typeof useGLSLRenderer>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
renderer = useGLSLRenderer()
|
||||
renderer.init(100, 100)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('delays convertToBlob execution by the debounce period', () => {
|
||||
renderer.debouncedToBlob()
|
||||
|
||||
expect(mockConvertToBlob).not.toHaveBeenCalled()
|
||||
|
||||
vi.advanceTimersByTime(150)
|
||||
|
||||
expect(mockConvertToBlob).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('coalesces rapid calls into a single convertToBlob', () => {
|
||||
renderer.debouncedToBlob()
|
||||
vi.advanceTimersByTime(50)
|
||||
renderer.debouncedToBlob()
|
||||
vi.advanceTimersByTime(50)
|
||||
renderer.debouncedToBlob()
|
||||
|
||||
vi.advanceTimersByTime(150)
|
||||
|
||||
expect(mockConvertToBlob).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('cancelPendingBlob prevents the conversion from running', () => {
|
||||
renderer.debouncedToBlob()
|
||||
renderer.cancelPendingBlob()
|
||||
|
||||
vi.advanceTimersByTime(200)
|
||||
|
||||
expect(mockConvertToBlob).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('dispose cancels pending blob conversions', () => {
|
||||
renderer.debouncedToBlob()
|
||||
renderer.dispose()
|
||||
|
||||
vi.advanceTimersByTime(200)
|
||||
|
||||
expect(mockConvertToBlob).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { debounce } from 'es-toolkit'
|
||||
|
||||
import { detectPassCount } from '@/renderer/glsl/glslUtils'
|
||||
|
||||
const VERTEX_SHADER_SOURCE = `#version 300 es
|
||||
@@ -443,9 +445,18 @@ export function useGLSLRenderer(config: GLSLRendererConfig = DEFAULT_CONFIG) {
|
||||
return canvas.convertToBlob({ type: 'image/webp', quality: 0.92 })
|
||||
}
|
||||
|
||||
const DEBOUNCE_DELAY_MS = 150
|
||||
|
||||
const debouncedToBlob = debounce(() => toBlob(), DEBOUNCE_DELAY_MS)
|
||||
|
||||
function cancelPendingBlob(): void {
|
||||
debouncedToBlob.cancel()
|
||||
}
|
||||
|
||||
function dispose(): void {
|
||||
if (disposed) return
|
||||
disposed = true
|
||||
cancelPendingBlob()
|
||||
if (!gl) return
|
||||
|
||||
for (const tex of inputTextures) {
|
||||
@@ -497,6 +508,8 @@ export function useGLSLRenderer(config: GLSLRendererConfig = DEFAULT_CONFIG) {
|
||||
render,
|
||||
readPixels,
|
||||
toBlob,
|
||||
debouncedToBlob,
|
||||
cancelPendingBlob,
|
||||
dispose
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user