Reputation: 53
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);
};
Upvotes: 1
Views: 3112
Reputation: 652
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.
Upvotes: 2
Reputation: 618
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/
Upvotes: 0