简体   繁体   中英

Webworker canvas performance terrible

I'm trying to use webworkers to render parts of the frames for an animated mandelbrot zoomer, since there is a lot of calculating involved, and since this can be easily split up in blocks this should be an ideal situation for parallel processing.

But no matter what I try I do not get any performance in return for the extra cpu the workers use. Compared to a non worker version, in Chrome my benchmark is somewhat slower, in Firefox it is much slower.

My guess is that transferring the image data to the webworkers is incredibly expensive, I tried just receiving raw data and using that to render frames but the result is much the same. I don't think this is the ideal way to send and receive imagedata to the workers (in fact I only need to receive it, but I have not been able to create a buffer inside the workers that can be used for the canvas directly). So at it stands sending any serious amount of data creates a real bottleneck.

Dear stackoverflow, please help me answer these two questions: What am I doing wrong here, and what can be improved?

A demo can be found here for workers, and for reference a non worker version on jsfiddle .

Code is as follows:

"use strict";

/*global $*/

$(function() {

    var mandelbrot = new Mandelbrot();

});

var Mandelbrot = function() {

    // set some values
    this.width = 500;
    this.height = 500;

    this.x_center = -1.407566731001088;
    this.y_center = 2.741525895538953e-10;

    this.iterations = 250;
    this.escape = 4,
    this.zoom = 10;
    this.count = 0;
    this.worker_size = 10;
    this.received = 0;
    this.refresh = true;

    //let's go - create canvas, image data and workers
    this.init();
    //start animation loop
    this.animate();

};

Mandelbrot.prototype = {

    init: function() {

        var self = this;

        //create main canvas and append it to div
        var container = $("#content");

        this.canvas = document.createElement("canvas");
        this.canvas.width = this.width;
        this.canvas.height = this.height;

        container.append(this.canvas);

        //create imagedata
        this.context = this.canvas.getContext("2d");
        this.image = this.context.getImageData(0, 0, this.width, this.height);
        this.data = new Int32Array(this.image.data.buffer);

        //create imagedata for webworkers
        this.worker_data = this.context.getImageData(0, 0, this.width, this.height / this.worker_size);

        //create webworkers drop them in array
        this.pool = [];

        for (var i = 0; i < this.worker_size; i++) {

            this.pool[i] = new Worker("js/worker.js");
            this.pool[i].idle = true;
            this.pool[i].id = i;

            //on webworker finished 
            this.pool[i].onmessage = function(e) {

                self.context.putImageData(e.data, 0, self.height / self.worker_size * e.target.id);
                self.received++;

            };

        }
    },

    iterate: function() {

        for (var i = 0; i < this.pool.length; i++) {

            this.pool[i].postMessage({

                image: this.worker_data,
                id: this.pool[i].id,
                worker_size: this.worker_size,
                width: this.width,
                height: this.height,
                x_center: this.x_center,
                y_center: this.y_center,
                iterations: this.iterations,
                escape: this.escape,
                zoom: this.zoom

            });
        }
    },

    animate: function() {

        requestAnimationFrame(this.animate.bind(this));

        //poor man's benchmark over 250 frames
        if (this.count === 0) {
            console.time("timer");
        } 

        if (this.count === 250) {
            console.timeEnd("timer");
        }

        //refresh at init, then refresh when all webworkers are done and reset
        if (this.received === this.worker_size | this.refresh) {

            this.received = 0;
            this.refresh = false;
            this.count++;
            this.zoom *= 0.95;
            this.iterate();

        }
    }
};

and worker.js:

self.onmessage = function(e) {

    "use strict";

    var x_step = e.data.zoom / e.data.width;
    var y_step = e.data.zoom / e.data.height;

    var y_start = e.data.height / e.data.worker_size * e.data.id;
    var y_end = e.data.height / e.data.worker_size;

    var data = new Int32Array(e.data.image.data.buffer);

    for (var y = 0; y < y_end; y++) {

        var iy = e.data.y_center - e.data.zoom / 2 + (y + y_start) * y_step;

        for (var x = 0; x < e.data.width; x++) {

            var rx = e.data.x_center - e.data.zoom / 2 + x * x_step;

            var zx = rx;
            var zy = iy;
            var zx2 = 0;
            var zy2 = 0;

            for (var i = 0; zx2 + zy2 < e.data.escape && i < e.data.iterations; ++i) {

                zx2 = zx * zx;
                zy2 = zy * zy;
                zy = (zx + zx) * zy + iy;
                zx = zx2 - zy2 + rx;
            }

            data[y * e.data.width + x] = (255 << 24) | (i << 16) | (i << 8) | i;

        }
    }

    self.postMessage(e.data.image);

};

The problem is that you are iterating over every pixel in the parent picture. If you restrict the iteration to the smaller of the two images, things will be much faster. Also, if you tile the drawing, each tile could be handled in a separate web worker, thus increasing the palletization of each section of the image. I wrote this: http://robertleeplummerjr.github.io/CanvasWorker/ which does exactly what you want.

I actually tried the same thing on this experiment, this is a displacement filter:

http://www.soundstep.com/blog/experiments/displacement-js/heart/ http://www.soundstep.com/blog/2012/04/25/javascript-displacement-mapping/

I created a worker in the filter and I compute the pixel together before posting them back to the main app. Basically iterating on all the pixels inside a worker.

Before the worker, I have in a loop 4 getImageData, this can't be done in the worker. It takes around 15% CPU on chrome no matter what.

So, overall I get 70% CPU without the worker, and I get 90% CPU with the worker.

I suppose the actions that cannot be done in the worker, such as "getImageData" AND "putImageData", plus the fact of having the worker itself, takes more CPU than not having a worker.

It would probably be better if we were able to send other types of data so we could do the getImageData and putImageData inside the worker.

Not sure there's another way sending and receiving bytes to treat and reconstruct the canvas content.

http://typedarray.org/concurrency-in-javascript/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM