Compare commits

...

1 Commits

Author SHA1 Message Date
bymyself
bc2dd1b6a2 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.
2026-06-19 14:42:30 -07:00
2 changed files with 75 additions and 1 deletions

View File

@@ -166,6 +166,10 @@ vi.stubGlobal(
}
)
const mockConvertToBlob = vi.fn(() =>
Promise.resolve(new Blob(['fake'], { type: 'image/webp' }))
)
vi.stubGlobal(
'OffscreenCanvas',
class {
@@ -181,7 +185,7 @@ vi.stubGlobal(
: null
}
convertToBlob() {
return Promise.resolve(new Blob(['fake'], { type: 'image/webp' }))
return mockConvertToBlob()
}
}
)
@@ -673,3 +677,60 @@ describe('useGLSLRenderer', () => {
})
})
})
describe('useGLSLRenderer debounced toBlob', () => {
let renderer: ReturnType<typeof useGLSLRenderer>
beforeEach(() => {
mockGL = createMockGLContext()
vi.useFakeTimers()
vi.clearAllMocks()
vi.mocked(detectPassCount).mockReturnValue(1)
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()
})
})

View File

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