Posted on June 4, 2022
Mandlebrot Generator
I was watching James Sharman’s video about benchmarking his hombrew CPU recently. In that video he generates a mandlebrot set as a benchmark. I realized I’d never written an implementation of mandlebrot, so thought it might be fun. I probably should have started from scratch, but instead I just copied the apple basic version from the multi-mandlebrot repo, which was the base of the benchmark (the apple basic one seemed the easiest to understand).
This was my first semi-working attempt. The code actually takes long enough to run that I was getting script timeouts, so this version does each render loop on a timeout. It made rendering a lot slower, but does prevent the browser from locking up.
You’ll note that this doesn’t look quite right. I thought it might have been the low resolution, or the way I did the mapping to canvas. I messed around with various params, but it took an embarrassingly long time to realized I’d simply added two values instead of subtracting them.
This version is still quite slow, however. It’s because we are doing a timeout after every loop, which introduces a lot of delay. To speed things up we can instead try do do as much work as possible in one loop, then stop and call ourselves again with a timeout (this allows the browser to run the event loop, giving time to respond to events and avoid the slow script issues).
This is reasonably fast, even at higher resolutions & iterations. However I was curious if we used a web worker if I could make things even faster. Since web workers run on their own thread, we don’t have to worry about locking up the browser and can just run in a single loop. One downside to the worker is we have to communicate all of our results via postMessage
which means a copy of each message has to be made. If we do this for each point it actually ends up being slower than our original implementation (presumably due to the overhead of copying). We could just send all the points when the loop is done, but then we can’t see any progress. After a little experimentation sending 50 points at at time seemed a reasonable compromise. (note I had to move off jsfiddle to allow use of the worker)
Perceptually the worker seems slower, however if the “ramp” setting is enabled (which re-draws the image with increasingly more iterations) it’s fairly clear the worker is faster. It’s possible we could get even faster using transferable objects or OffscreenCanvas
(not to mention potentially optimizing the algorithm itself) but this “final” version is good enough for me.