The goal here is to demonstrate parallelism on a single web page as well as distributed parallelism via Firebase.
We are going to compute a subset of the Mandelbrot Set and render it in a grid of <div>
s. This is a Work in Progress and I’m still trying to get good performance from the Firebase version. Using a grid of divs is not the fastest way to draw. Using a canvas would be faster and cooler.
These examples, especially the AngularFire example, illustrate some of the limitations of Angular when dealing with large numbers of watched variables. The correct way to do this is to compute Canvas-drawing instructions from the Firebase information, rather than updating the DOM incrementally.
Local Sharing Test
We’re gonna start by seeing if we can share a 2D matrix of numbers via Angular data bindings. That will let me have a few machines (one at first) work on the problem in parallel. Next, we’ll see if the idea of sharing a local Angular 2D matrix extends to sharing a data-bound Firebase matrix via AngularFire.
The algorithm below is straighforward and computes each cell from the upper left to the lower right, one row at a time.
function computeMandelCell(matrixX, matrixY) {
var real = $scope.lowReal + ($scope.tickReal * matrixX);
var im = $scope.lowIm + ($scope.tickIm * matrixY);
var maxIter = $scope.maxIter;
var result = real * im;
var i;
var x = 0.0;
var y = 0.0;
for (i = 0; i < maxIter; ++i)
{
var tmp = 2*x*y;
x = x*x - y*y + real;
y = tmp + im;
if (x*x + y*y > 4)
{
return i;
}
}
return 0;
}
$scope.resetCells = function() {
$scope.cells = [];
for (var x = 0; x < $scope.dimReal; ++x)
{
$scope.cells[x] = [];
for (var y = 0; y < $scope.dimIm; ++y)
{
$scope.cells[x][y] = {completed: 0, color: "#000000"};
}
}
};
$scope.computeAvailableCell = function() {
for (var x = 0; x < $scope.dimReal; ++x)
{
var row = $scope.cells[x];
for (var y = 0; y < $scope.dimIm; ++y)
{
var cell = row[y];
if (cell.completed == 0)
{
var cellVal = computeMandelCell(y, x);
var hue = 180;
var saturation = 100;
var lightness = 20;
var opacity = 1.0;
if (cellVal > 0)
{
var maxIter = $scope.maxIter;
var val = (cellVal + 1.0) / maxIter;
hue = Math.floor(360.0 * 3 * val);
lightness = 40 + Math.floor(60.0 * val);
}
else
{
lightness = 0;
}
cell.color = "hsla(" + hue + "," + saturation + "%," + lightness + "%, " + opacity + ")";
cell.completed = 1;
return true;
}
}
}
return false;
};
$scope.resetCells();
} angular .module(‘BlogApp’) .controller(‘ControllerA’, ControllerA);
The code for the above should be extracted into a separate .js
file for the purpose of building a web application, but this blog is all about one-page experimental apps and I haven’t found a good convention for this yet.
<div>
<script>
function ControllerA($scope, $firebase) {
$scope.lowReal = -0.5;
$scope.highReal = 0;
$scope.dimReal = 25.0;
$scope.tickReal = ($scope.highReal - $scope.lowReal) / $scope.dimReal;
$scope.lowIm = -1;
$scope.highIm = -0.5;
$scope.dimIm = 25.0;
$scope.tickIm = ($scope.highIm - $scope.lowIm) / $scope.dimIm;
$scope.maxIter = 250;
function computeMandelCell(matrixX, matrixY) {
var real = $scope.lowReal + ($scope.tickReal * matrixX);
var im = $scope.lowIm + ($scope.tickIm * matrixY);
var maxIter = $scope.maxIter;
var result = real * im;
var i;
var x = 0.0;
var y = 0.0;
for (i = 0; i < maxIter; ++i)
{
var tmp = 2*x*y;
x = x*x - y*y + real;
y = tmp + im;
if (x*x + y*y > 4)
{
return i;
}
}
return 0;
}
$scope.resetCells = function() {
$scope.cells = [];
for (var x = 0; x < $scope.dimReal; ++x)
{
$scope.cells[x] = [];
for (var y = 0; y < $scope.dimIm; ++y)
{
$scope.cells[x][y] = {completed: 0, color: "#000000"};
}
}
};
$scope.computeAvailableCell = function() {
for (var x = 0; x < $scope.dimReal; ++x)
{
var row = $scope.cells[x];
for (var y = 0; y < $scope.dimIm; ++y)
{
var cell = row[y];
if (cell.completed == 0)
{
var cellVal = computeMandelCell(y, x);
var hue = 180;
var saturation = 100;
var lightness = 20;
var opacity = 1.0;
if (cellVal > 0)
{
var maxIter = $scope.maxIter;
var val = (cellVal + 1.0) / maxIter;
hue = Math.floor(360.0 * 3 * val);
lightness = 40 + Math.floor(60.0 * val);
}
else
{
lightness = 0;
}
cell.color = "hsla(" + hue + "," + saturation + "%," + lightness + "%, " + opacity + ")";
cell.completed = 1;
return true;
}
}
}
return false;
};
$scope.resetCells();
}
angular
.module('BlogApp')
.controller('ControllerA', ControllerA);
</script>
<div style="background-color: #EFEFEF;width:400px;" ng-controller="ControllerA">
<h6>Machine Time: {{ MA.getMachineTime() }}</h6>
<h6>Machine State: {{ MA.getMachineState() }}</h6>
<h6>Machine Running: {{ MA.isRunning() }}</h6>
<mlmmachine
ptr="MA"
ng-init="messageNumber=0;"
reset-fn="resetCells(); MA.setTickMS(1);"
step-fn="computeAvailableCell() ? 0 : MA.pauseMachine()"
class="mlm background">
<mlmpanel></mlmpanel>
</mlmmachine>
<style type="text/css">
.gridtable {
background-color: #000000;
display:table;
width:100%;
}
.gridrow {
display:table-row;
width:100%;
}
.gridcell {
display:table-cell;
width:4px;
height:4px;
float:left;
}
</style>
<br/>
<div class="gridtable">
<div ng-repeat="row in cells">
<div class="gridrow">
<div class="gridcell"></div>
<div class="gridcell"></div>
<div class="gridcell"></div>
<div ng-repeat="cell in row" style="float:left;">
<div class="gridcell" style="background-color:{{cell.color}};">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
Firebase Sharing Test
So now a crude modification to the above that maps the same matrix used above onto a Firebase ref.
I originally wrote this by mapping the 2D matrix onto Firebase, but it acted incorrectly and resulted in the picture being sliced oddly (but consistently). I’ve since written the code to map the 2D cell matrix onto a 1D Firebase array, which produces better results.
I may revisit the 2D solution again one day when I have time to fully grok AngularFire.
Clear Grid
The code for the above (yes, this should be moved into a separate file):
<script>
function ControllerB($scope, $firebase) {
$scope.lowReal = -0.5;
$scope.highReal = 0;
$scope.dimReal = 25.0;
$scope.tickReal = ($scope.highReal - $scope.lowReal) / $scope.dimReal;
$scope.lowIm = -1;
$scope.highIm = -0.5;
$scope.dimIm = 25.0;
$scope.tickIm = ($scope.highIm - $scope.lowIm) / $scope.dimIm;
$scope.maxIter = 250;
var cellsSubscripts = [];
for (var x = 0; x < $scope.dimReal; ++x)
{
cellsSubscripts[x] = [];
for (var y = 0; y < $scope.dimIm; ++y)
{
cellsSubscripts[x][y] = {x: x, y: y};
}
}
$scope.cellsSubscripts = cellsSubscripts;
var ref = new Firebase('https://doctorbud.firebaseIO.com/mandelbot2');
$scope.cells2 = $firebase(ref);
function computeMandelCell(matrixX, matrixY) {
var real = $scope.lowReal + ($scope.tickReal * matrixX);
var im = $scope.lowIm + ($scope.tickIm * matrixY);
var maxIter = $scope.maxIter;
var result = real * im;
var i;
var x = 0.0;
var y = 0.0;
for (i = 0; i < maxIter; ++i)
{
var tmp = 2*x*y;
x = x*x - y*y + real;
y = tmp + im;
if (x*x + y*y > 4)
{
return i;
}
}
return 0;
}
$scope.resetCells = function() {
var cells2 = [];
for (var x = 0; x < $scope.dimReal; ++x)
{
for (var y = 0; y < $scope.dimIm; ++y)
{
var idx = x * $scope.dimReal + y;
cells2[idx] = {completed: 0, color: "#000000"};
}
}
$scope.cells2.$set(cells2);
};
$scope.computeAvailableCell = function() {
for (var x = 0; x < $scope.dimReal; ++x)
{
for (var y = 0; y < $scope.dimIm; ++y)
{
var idx = x * $scope.dimReal + y;
var cell = $scope.cells2[idx];
if (cell.completed === 0)
{
var cellVal = computeMandelCell(y, x);
var hue = 180;
var saturation = 100;
var lightness = 20;
var opacity = 1.0;
if (cellVal > 0)
{
var maxIter = $scope.maxIter;
var val = (cellVal + 1.0) / maxIter;
hue = Math.floor(360.0 * 3 * val);
lightness = 40 + Math.floor(60.0 * val);
}
else
{
lightness = 0;
}
$scope.cells2[idx].color = "hsla(" + hue + "," + saturation + "%," + lightness + "%, " + opacity + ")";
$scope.cells2[idx].completed = 1;
$scope.cells2.$save();
return true;
}
}
}
return false;
};
// $scope.resetCells();
}
</script>
<div style="background-color: #EFEFEF;width:400px;" ng-controller="ControllerB">
<h6>Machine Time: {{ MB.getMachineTime() }}</h6>
<h6>Machine State: {{ MB.getMachineState() }}</h6>
<h6>Machine Running: {{ MB.isRunning() }}</h6>
<button class="btn btn-default btn-xs p-0" title="Clear Grid" ng-click="resetCells()">Clear Grid</button>
<mlmmachine
ptr="MB"
ng-init="messageNumber=0;MB.resetMachine()"
reset-fn="MB.setTickMS(1);"
step-fn="computeAvailableCell() ? 0 : MB.pauseMachine()"
class="mlm background">
<mlmpanel></mlmpanel>
</mlmmachine>
<style type="text/css">
.gridtable {
background-color: #000000;
display:table;
width:100%;
}
.gridrow {
display:table-row;
width:100%;
}
.gridcell {
display:table-cell;
width:8px;
height:8px;
float:left;
}
</style>
<br/>
<div class="gridtable">
<div ng-repeat="row in cellsSubscripts">
<div class="gridrow">
<div class="gridcell"></div>
<div class="gridcell"></div>
<div class="gridcell"></div>
<div ng-repeat="cell in row" style="float:left;">
<div class="gridcell" style="background-color:{{ cells2[cell.x * dimReal + cell.y].color }};">
</div>
</div>
</div>
</div>
</div>
</div>