Monday, August 28, 2023

Rust/WebAssembly vs Javascript performance on Mandelbrot animation rendering

The pending question Is Rust/Wasm fast enough to even bother when comparing to Javascript? has now yet another biased benchmark. I've decided to take one of my Julia/Mandelbrot demos, benchmark it "as-is" and then, port to Rust and compare.
The HTML/Javascript source is short enough to post it here, the original demo code has some additional features (zooming) which are not present here
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Mandelbrot</title>
	<script type="text/javascript">
	        
        var cx = 0, cy = 0, kat1 = 0, kat2 = 1;           
        
        var WJulia = 1024;
        var HJulia = 1024;
        var frame  = 0;
        var startDate;
    
        var contextJulia;
        var contextSmall;
        var pixJulia;
        var imgdJulia;

        var iStart;
        var iEnd;
        var jStart;
        var jEnd;

        var iDelta;
        var jDelta;
        
        function setInitialScale()
        {
            iStart = -1.92;
            iEnd   = 1.92;
            jStart = -1.92;
            jEnd   = 1.92;

            iDelta = (iEnd-iStart)/1024;
            jDelta = (jEnd-jStart)/1024;

            startDate = new Date();
        }

        function SetupMandel()
        {	 
            var elemJulia = document.getElementById('JuliaCanvas');
            if (elemJulia && elemJulia.getContext)
            {
                contextJulia = elemJulia.getContext('2d');
                if (contextJulia)
                {
                    imgdJulia = contextJulia.createImageData(WJulia, HJulia);         
                    pixJulia = imgdJulia.data;
                }
            }

            setInitialScale();
          
            /* start */
            requestAnimationFrame( LoopMandel );
        }
                         
        function LoopMandel()
        {	 
            kat1 += 0.0021;
            kat2 += 0.0039;
            cx = .681 * Math.sin(kat1);
            cy = .626 * Math.cos(kat2);

            frame++;
            document.getElementById('fps').innerHTML = `FPS: ${1000*frame/(new Date()-startDate)}`;
			
            RysujMandel();	 
            
            requestAnimationFrame( LoopMandel );
        }
         
         
        /* tworzenie bitowego obrazu */
        function RysujMandel()
        {
            /* obliczenia */
            var wi, wj;
            var i, j;

            var iterations = 255;

            var px = 0;
            for (i = iStart, wi = 0; wi < 1024; i += iDelta, wi++)
            {
                var py = 0;
                for (j = jStart, wj = 0; wj < 1024; j += jDelta, wj++)
                {
                    var c = 0;

                    var x = cx;
                    var y = cy;
         
                    while (((x*x + y*y) < 4) && (c < iterations))
                    {
                        [x, y] = [x * x - y * y + i, 2 * x * y + j];
                        c++;
                    }

                    SetPixelColor( pixJulia, (py * WJulia + px) << 2, 255, 255-c, 255-c, 255 - (c/2) );
                                        
                    py++;
                }
         
                px++;
            }
            
            /* kopiowanie bitowego obrazu do context/canvas */
            contextJulia.putImageData(imgdJulia, 0, 0);		 
        }
         
        function SetPixelColor(pix,offs, a, r, g, b)
        {            
            pix[offs++] = r;
            pix[offs++] = g;
            pix[offs++] = b;
            pix[offs] = a;
        }
         
        window.addEventListener( 'load', SetupMandel );
        
        </script>    
</head>
<body>
    <span id="fps" style='display:block'></span>
    <canvas id="JuliaCanvas" width="1024" height="1024">
	</canvas>
</body>
</html>
When run on my machine and the Edge Browser, it makes ~10-11 frames per second. Not bad assuming 1024x1024 image and 255 iterations.
Same code ported to Rust and compiled with wasm-pack
// https://rustwasm.github.io/wasm-bindgen/examples/dom.html
use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped;
use web_sys::{CanvasRenderingContext2d, ImageData};

/* tworzenie bitowego obrazu */
#[wasm_bindgen]
pub fn mandel(
    ctx: &CanvasRenderingContext2d, 
    width: u32, height: u32,
    cx: f64, cy: f64) -> Result<(), JsValue> {

    let i_start = -1.92;
    let i_end   = 1.92;
    let j_start = -1.92;
    let j_end   = 1.92;

    let i_delta = (i_end-i_start)/width as f64;
    let j_delta = (j_end-j_start)/height as f64;

    /* obliczenia */    
    let iterations = 255;

    let mut pix_julia = Vec::with_capacity( usize::try_from(width * height).unwrap() );

    let mut j = j_start;
    for _wj in 0..height {

        let mut i = i_start;   
        for _wi in 0..width {
        
            let mut c: u8 = 0;

            let mut x = cx;
            let mut y = cy;
    
            while ( (x*x + y*y) < 4.0) && (c < iterations)
            {
                let _tx = x;
                x = x * x - y * y + i;
                y = 2.0 * _tx * y + j;
                c += 1;
            }

            pix_julia.push( 255-c );
            pix_julia.push( 255-c );
            pix_julia.push( 255-(c/2) );
            pix_julia.push( 255 );
            
            i  += i_delta;
        }
    
        j  += j_delta;
    }    

    let data = ImageData::new_with_u8_clamped_array_and_sh(Clamped(&pix_julia), width, height)?;
    ctx.put_image_data(&data, 0.0, 0.0)
}
The cargo.tomlwas
[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.87"

[dependencies.web-sys]
version = "0.3.4"
features = [
  'Document',
  'Element',
  'HtmlElement',
  'Node',
  'Window',
  'ImageData',
  'CanvasRenderingContext2d'  
]

[profile.release]
opt-level = 3
lto = true

[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-O4", "--enable-mutable-globals"]
and the HTML
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Mandelbrot</title>
  </head>
  <body>
    <script type="module">
        import init, { mandel } from "./pkg/hello_wasm.js";
        
        (async function() {
		   await init();
           SetupMandel();    
        })();
	  
        var cx = 0, cy = 0, kat1 = 0, kat2 = 1;      
        var WJulia = 1024;
        var HJulia = 1024;        
        var frame  = 0;
		var startDate;
        
        var contextJulia;
        var contextSmall;
        var pixJulia;
        var imgdJulia;

        function SetupMandel()
        {	 
            var elemJulia = document.getElementById('JuliaCanvas');
            if (elemJulia && elemJulia.getContext)
            {
                contextJulia = elemJulia.getContext('2d');
                if (contextJulia)
                {
                    imgdJulia = contextJulia.createImageData(WJulia, HJulia);         
                    pixJulia = imgdJulia.data;
                }
            }
          
		    startDate = new Date();
			
            /* start */
            requestAnimationFrame( LoopMandel );
        }
                         
        function LoopMandel()
        {	 
            kat1 += 0.0021;
            kat2 += 0.0039;
            cx = .681 * Math.sin(kat1);
            cy = .626 * Math.cos(kat2);

            frame++;
            document.getElementById('fps').innerHTML = `FPS: ${1000*frame/(new Date()-startDate)}`;
			
            mandel(contextJulia, WJulia, HJulia, cx, cy);	 
            
            requestAnimationFrame( LoopMandel );
        }	  
    </script>
    <span id="fps" style='display: block'></span>	
    <canvas id="JuliaCanvas" width="1024" height="1024">
	</canvas>	
  </body>
</html>
And the result? Similar, it also makes 10-11 frames per second. Additional tests (different resolution and number of iterations) and possible optimizations can be applied here.