samizdatco / skia-canvas

A GPU-accelerated 2D graphics environment for Node.js
MIT License
1.67k stars 63 forks source link

Exporting is terribly slow #152

Open hexaclue opened 11 months ago

hexaclue commented 11 months ago

Using any of the methods to save the canvas to a file is extremely slow compared to node-canvas.

My render loop would take an average speed of 20-30 fps using node-canvas, whereas skia-canvas takes almost four times as long with only 5 fps (sometimes even lower, like 2).

Removing any of canvas.saveAs, canvas.toBuffer or canvas.toDataURLSync from my skia-canvas-using code speeds up the render loop to over 1400 fps, indicating the problem definitely lays in the exporting of the canvas. Even when using just canvas.toBuffer without writing to the disk slows the render loop down.

The performance drop is observed to be the same, no matter what format ("jpg", "png") is used or whether canvas.gpu is enabled or not.

vincaslt commented 6 months ago

What methods have you been comparing?

From my own tests it seems like skia-canvas exports png images quite a bit faster than node-canvas. However, there's no toBuffer('raw') option, and manually converting image data to buffer then saving to file was slightly slower than node-canvas. I wonder if it's possible to have toBuffer export raw pixels, somehow optimizing it in similar ways like png/jpeg exports.

mpaperno commented 6 months ago

@vincaslt Repeating myself, but it seems relevant here as well -- toImageData() is synchronous, which is why it appears to be slower (for a canvas with many elements it is a significant difference). The other export options use new threads (from Node thread pool) to produce the final result, so they're not just async but actually multi-threaded as well.

As for the OP question -- personally I think that makes a lot more sense than rendering the whole image every time a new layer/element is added. Typically one doesn't need the final image until it is output, in some fashion, so why bother rendering any intermediate stages? As for comparing with other canvas implementations, I imagine the final result could depend on a number of factors -- actual code/benchmark would be most relevant here.

kostia1st commented 1 week ago

I'm not sure if it's relevant for the issue of the topic starter, but I saw the notion of "5 fps", so it might be the case.

I was drawing a lot of frames for a video by just reusing the same canvas. Each frame gets exported, and the next frame gets drawn on top of the previous one(s). And eventually I noticed that exporting the first frames was times (tens of times) faster than exporting the last ones. That was weird. And then I realized.

If you think of the canvas as of a raster pixel matrix, then there's no issue with this approach. Draw -> export -> rewrite on top -> export - must be OK. But once you learn it's all vector, it's a completely different story. It turns out I was eventually adding thousands of complex objects into the same canvas without ever clearing them. Then it totally makes sense that computing what must actually be visible in the end raster image takes longer and longer and longer the more objects you add.

TLDR the solution was strikingly simple. Just ctx.reset() at the beginning of each new frame drawing procedure, and suddenly the frame generation process became linear in performance, in my case approximately 30fps (0.98 jpeg quality).

Hope this helps.

EDIT: My thinking of skia-canvas as a vector canvas might be wrong of course, but the result seems to prove that it collects some internal state over time, and that state takes very long to render once it's grown big.