Skip to content

fix(renderer): delete CanvasKit Shader/PathEffect/ColorFilter objects to prevent WASM heap OOM#128

Open
Postroggy wants to merge 1 commit into
ZSeven-W:mainfrom
Postroggy:fix/renderer-canvaskit-wasm-memory-leak
Open

fix(renderer): delete CanvasKit Shader/PathEffect/ColorFilter objects to prevent WASM heap OOM#128
Postroggy wants to merge 1 commit into
ZSeven-W:mainfrom
Postroggy:fix/renderer-canvaskit-wasm-memory-leak

Conversation

@Postroggy
Copy link
Copy Markdown

Problem

When rendering a large number of nodes (e.g. 800+ component variants from a Figma library), the CanvasKit WASM heap grows linearly until it is exhausted, crashing with:

RuntimeError: Aborted()

The crash occurs after rendering ~100–200 nodes depending on component complexity.

Root Cause

CanvasKit uses manual ref-counted memory on the WASM heap. Every object created via ck.Shader.MakeLinearGradient(...), ck.PathEffect.MakeDash(...), ck.ColorFilter.MakeMatrix(...), etc. must be explicitly .delete()d.

The bug: after calling paint.setShader(shader) / paint.setPathEffect(effect) / paint.setColorFilter(cf), the JS-side handle was never deleted. paint.delete() only decrements the Paint's own ref — the underlying Skia object held by the JS variable is never freed.

Internally, paint.setShader(shader) calls SkRef() on the C++ side (refcount → 2). When the paint is deleted, refcount drops to 1. Without shader.delete(), the WASM heap object is permanently leaked. Across hundreds of render calls this exhausts the WASM heap.

Fix

Add .delete() immediately after each set*() call — safe because the Paint holds its own internal SkRef. 13 sites fixed in node-renderer.ts:

Method Object type Location
makeFillPaint Shader (linear gradient) line ~175
makeFillPaint Shader (radial gradient) line ~201
applyImageFillToPaint Shader (tile image) line ~281
applyImageFillToPaint ColorFilter (image adjust) line ~284
drawImageFillRect ColorFilter (image adjust) line ~316
makeStrokePaint PathEffect (dash) line ~437
drawEllipse PathEffect (corner) × 2 lines ~687, ~695
drawPolygon PathEffect (corner) × 2 lines ~780, ~788
drawImage ColorFilter (image adjust) line ~1028
drawImage Shader (tile image) line ~1041
drawImageFallback PathEffect (dash) line ~1116

Pattern applied

// Before (leaked)
const shader = ck.Shader.MakeLinearGradient(...);
if (shader) paint.setShader(shader);

// After (fixed)
const shader = ck.Shader.MakeLinearGradient(...);
if (shader) { paint.setShader(shader); shader.delete(); }

Verification

Tested rendering 800+ Figma component variants in a single batch run. Before fix: crash at ~181 nodes. After fix: all nodes render successfully with stable memory usage.

… to prevent WASM heap OOM

CanvasKit uses manual memory management — every object created via the
CanvasKit API must be explicitly `.delete()`d. When a JS-side reference
to a CanvasKit object is held (e.g. `const shader = ck.Shader.Make...()`),
calling `paint.delete()` only decrements the paint's own ref count on the
WASM-side object; the JS wrapper still holds a ref, so the WASM heap object
is never freed.

This caused a linear WASM heap growth during batch rendering (e.g. offline
.fig export), crashing with `RuntimeError: Aborted()` after ~100 nodes.

Fix: call `.delete()` on the JS-side handle immediately after handing
ownership to the paint via `set*()`. The paint holds its own internal ref
(via SkRef), so deleting the JS wrapper is safe and correct.

Affected sites (13 total):
- `makeFillPaint`: linear gradient Shader, radial gradient Shader
- `applyImageFillToPaint`: tile image Shader, ColorFilter from buildImageAdjustmentFilter
- `drawImageFillRect`: ColorFilter from buildImageAdjustmentFilter
- `makeStrokePaint`: PathEffect.MakeDash
- `drawEllipse`: PathEffect.MakeCorner ×2 (fill + stroke)
- `drawPolygon`: PathEffect.MakeCorner ×2 (fill + stroke)
- `drawImage`: ColorFilter from buildImageAdjustmentFilter, tile Shader
- `drawImageFallback`: PathEffect.MakeDash
@Kayshen-X
Copy link
Copy Markdown
Contributor

Thank you for your submission, but we are rewriting this program using Rust, so your submission may be temporarily irrelevant. You can refer to the v0.8.0 branch if you're interested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants