Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

drawImage performance bug with canvas #972

Open
monteslu opened this issue Jan 13, 2025 · 6 comments · Fixed by #984
Open

drawImage performance bug with canvas #972

monteslu opened this issue Jan 13, 2025 · 6 comments · Fixed by #984

Comments

@monteslu
Copy link

Drawing a canvas onto a canvas is almost 8 times slower than drawing an image onto a canvas:

import { createCanvas, loadImage } from '@napi-rs/canvas';


const canvas = createCanvas(1920, 1080);
const ctx = canvas.getContext('2d');

const iterations = 100;

async function main() {

  const image = await loadImage('./640x480_image.png');

  const canvas2 = createCanvas(640, 480);
  const ctx2 = canvas2.getContext('2d');
  ctx2.fillStyle = 'white';
  ctx2.fillRect(0, 0, canvas2.width, canvas2.height);

  let start = performance.now();
  for (let i = 0; i < iterations; i++) {
    ctx2.drawImage(image, 0, 0, canvas.width, canvas.height);
  }
  let end = performance.now();
  console.log('average draw image time', (end - start) / iterations);

  start = performance.now();
  for (let i = 0; i < iterations; i++) {
    ctx.drawImage(canvas2, 0, 0, canvas.width, canvas.height);
  }
  end = performance.now();
  console.log('average draw canvas time', (end - start) / iterations);
}

main();

outputs:
average draw image time 1.2156541699999999
average draw canvas time 8.897838329999999

On the web the two draw in about the same amount of time. 7 milliseconds can make a huge amount of difference when you might have a 16 millisecond budget rendering at 60Hz.

My stab in the dark guess is that somewhere along the way there's a mem copy happening.
Something similar to: Buffer.from(imageData.data.buffer)) vs Buffer.from(imageData.data))

@monteslu
Copy link
Author

ran benchmark on my mac pro 14 M2 Max 64G

Draw a House and export to PNG
┌─────────┬─────────────────┬───────────────────────┬──────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
│ (index) │ Task name       │ Latency average (ns)  │ Latency median (ns)      │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
├─────────┼─────────────────┼───────────────────────┼──────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
│ 0       │ '@napi-rs/skia' │ '19647577.48 ± 1.72%' │ '19294625.00 ± 26708.00' │ '51 ± 1.68%'               │ '52'                      │ 64      │
│ 1       │ 'skia-canvas'   │ '19991857.44 ± 2.72%' │ '19301208.50 ± 10708.50' │ '50 ± 1.80%'               │ '52'                      │ 64      │
│ 2       │ 'node-canvas'   │ '22529859.37 ± 1.52%' │ '21699937.50 ± 8062.50'  │ '45 ± 1.43%'               │ '46'                      │ 64      │
└─────────┴─────────────────┴───────────────────────┴──────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘
Draw Gradient and export to PNG
┌─────────┬─────────────────┬───────────────────────┬──────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
│ (index) │ Task name       │ Latency average (ns)  │ Latency median (ns)      │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
├─────────┼─────────────────┼───────────────────────┼──────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
│ 0       │ '@napi-rs/skia' │ '20375752.00 ± 1.57%' │ '19875125.00 ± 17542.00' │ '49 ± 1.50%'               │ '50'                      │ 64      │
│ 1       │ 'skia-canvas'   │ '21162263.69 ± 1.37%' │ '20728291.50 ± 9666.50'  │ '47 ± 1.32%'               │ '48'                      │ 64      │
│ 2       │ 'node-canvas'   │ '24238384.72 ± 1.20%' │ '24197770.50 ± 23062.50' │ '41 ± 1.19%'               │ '41'                      │ 64      │
└─────────┴─────────────────┴───────────────────────┴──────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘

then added a canvas on canvas draw to both tests:

// start of drawGradient() and drawHouse()
 const canvas = factory(1024, 768)
 const canvas2 = factory(1920, 1080)

 const ctx = canvas.getContext('2d')!
 const ctx2 = canvas2.getContext('2d')!

// the test's  canvas drawing commands


// draw the canvas on a canvas
ctx2.drawImage(canvas, 0, 0, canvas2.width, canvas2.height)

// rest of test
Draw a House and export to PNG
┌─────────┬─────────────────┬───────────────────────┬──────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
│ (index) │ Task name       │ Latency average (ns)  │ Latency median (ns)      │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
├─────────┼─────────────────┼───────────────────────┼──────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
│ 0       │ '@napi-rs/skia' │ '29897975.89 ± 1.59%' │ '29156979.00 ± 12896.00' │ '34 ± 1.52%'               │ '34'                      │ 64      │
│ 1       │ 'skia-canvas'   │ '19575501.30 ± 1.22%' │ '19075167.00 ± 1917.00'  │ '51 ± 1.13%'               │ '52'                      │ 64      │
│ 2       │ 'node-canvas'   │ '41313487.00 ± 1.88%' │ '40894791.50 ± 50958.50' │ '24 ± 1.78%'               │ '24'                      │ 64      │
└─────────┴─────────────────┴───────────────────────┴──────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘
Draw Gradient and export to PNG
┌─────────┬─────────────────┬───────────────────────┬──────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
│ (index) │ Task name       │ Latency average (ns)  │ Latency median (ns)      │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
├─────────┼─────────────────┼───────────────────────┼──────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
│ 0       │ '@napi-rs/skia' │ '32166183.55 ± 1.70%' │ '31983208.50 ± 6541.50'  │ '31 ± 1.70%'               │ '31'                      │ 64      │
│ 1       │ 'skia-canvas'   │ '20386656.27 ± 1.14%' │ '19968645.50 ± 145.50'   │ '49 ± 1.04%'               │ '50'                      │ 64      │
│ 2       │ 'node-canvas'   │ '41585669.28 ± 1.81%' │ '40705791.50 ± 63541.50' │ '24 ± 1.68%'               │ '25'                      │ 64      │
└─────────┴─────────────────┴───────────────────────┴──────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘

drops to about 62 to 65 % of skia-canvas performance

@monteslu
Copy link
Author

monteslu commented Feb 8, 2025

performance issue still present after 0.1.67

This is with my change to the benchmark above to draw canvas on canvas:

> @napi-rs/[email protected] bench
> node -r @swc-node/register benchmark/bench.ts

Draw a House and export to PNG
┌─────────┬─────────────────┬────────────────────┬────────────────────────┬────────────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name       │ Latency avg (ns)   │ Latency med (ns)       │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │
├─────────┼─────────────────┼────────────────────┼────────────────────────┼────────────────────────┼────────────────────────┼─────────┤
│ 0       │ '@napi-rs/skia' │ '29711492 ± 0.63%' │ '29434354 ± 327624.50' │ '34 ± 0.61%'           │ '34 ± 0'               │ 64      │
│ 1       │ 'skia-canvas'   │ '20080335 ± 0.56%' │ '20014708 ± 256520.50' │ '50 ± 0.54%'           │ '50 ± 1'               │ 64      │
│ 2       │ 'node-canvas'   │ '39692225 ± 0.55%' │ '39573355 ± 464937.50' │ '25 ± 0.54%'           │ '25 ± 0'               │ 64      │
└─────────┴─────────────────┴────────────────────┴────────────────────────┴────────────────────────┴────────────────────────┴─────────┘
Draw Gradient and export to PNG
┌─────────┬─────────────────┬────────────────────┬────────────────────────┬────────────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name       │ Latency avg (ns)   │ Latency med (ns)       │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │
├─────────┼─────────────────┼────────────────────┼────────────────────────┼────────────────────────┼────────────────────────┼─────────┤
│ 0       │ '@napi-rs/skia' │ '30835298 ± 0.87%' │ '30584875 ± 475833.00' │ '32 ± 0.80%'           │ '33 ± 1'               │ 64      │
│ 1       │ 'skia-canvas'   │ '21040808 ± 0.62%' │ '20856876 ± 226416.50' │ '48 ± 0.59%'           │ '48 ± 1'               │ 64      │
│ 2       │ 'node-canvas'   │ '40920705 ± 0.61%' │ '40896125 ± 608458.50' │ '24 ± 0.60%'           │ '24 ± 0'               │ 64      │
└─────────┴─────────────────┴────────────────────┴────────────────────────┴────────────────────────┴────────────────────────┴─────────┘

@Brooooooklyn can this issue be re-opened?

@Brooooooklyn Brooooooklyn reopened this Feb 9, 2025
@Brooooooklyn
Copy link
Owner

@monteslu I found that your measurement is false positive, ctx2.drawImage(canvas, 0, 0, canvas2.width, canvas2.height) this line does nothing in skia-canvas, skia-canvas does not draw anything immediately. You need to call canvas2.toBufferSync('png') to make it drawing things.

@Brooooooklyn
Copy link
Owner

Can confirm the skia-canvas is basically use the same drawing logic with in 0.1.65

Skia-canvas draw_image: https://github.com/samizdatco/skia-canvas/blob/v2.0.2/src/context/mod.rs#L478
rust-canvas: https://github.com/rust-skia/rust-skia/blob/0.81.0/skia-safe/src/core/canvas.rs#L1643
@napi-rs/canvas 0.16.5: https://github.com/Brooooooklyn/canvas/blob/v0.1.65/skia-c/skia_c.cpp#L344 (SkCanvas was converted into SkImage here like skia-canvas)

@monteslu
Copy link
Author

monteslu commented Feb 10, 2025

@monteslu I found that your measurement is false positive, ctx2.drawImage(canvas, 0, 0, canvas2.width, canvas2.height) this line does nothing in skia-canvas, skia-canvas does not draw anything immediately. You need to call canvas2.toBufferSync('png') to make it drawing things.

Ahh, that would explain why skia-canvas appears to be faster.

For some reference, I'm using this library along with https://github.com/kmamal/node-sdl to enable canvas based games on devices that don't have enough ram to run chrome. I'm not actually creating image files or streams.

//... do drawing commands with @napi-rs/canvas including canvas-on-canvas drawing

// get buffer from canvas.
const buffer = Buffer.from(backCanvas.data().buffer);

// libSDL window
appWindow.render(backCanvas.width, backCanvas.height, stride, 'rgba32', buffer);

0.1.65 and 0.1.67 correctly stretch/render to the underlying buffer. They're just significantly slower than canvas-on-canvas rendering in chrome or firefox.

This is the library I'm working on that uses @napi-rs/canvas and SDL: https://github.com/monteslu/jsgamelauncher

Here's a video that explains the js gamelauncher a little more: https://www.youtube.com/watch?v=osJsBRPSrM4

@monteslu
Copy link
Author

Can confirm the skia-canvas is basically use the same drawing logic with in 0.1.65

Skia-canvas draw_image: https://github.com/samizdatco/skia-canvas/blob/v2.0.2/src/context/mod.rs#L478 rust-canvas: https://github.com/rust-skia/rust-skia/blob/0.81.0/skia-safe/src/core/canvas.rs#L1643 @napi-rs/canvas 0.16.5: https://github.com/Brooooooklyn/canvas/blob/v0.1.65/skia-c/skia_c.cpp#L344 (SkCanvas was converted into SkImage here like skia-canvas)

Since I can get the canvas.data().buffer() really quickly and drawImage() with an Image seems to be a fast operation, Is there any way I can create an Image object with the data buffer directly? Maybe I can work around that casting operation.

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 a pull request may close this issue.

2 participants