Compare commits

...

1 Commits

4 changed files with 1800 additions and 0 deletions
Split View
  1. +23
    -0
      bandwidth_viz_test/crc32.js
  2. +536
    -0
      bandwidth_viz_test/index.html
  3. +45
    -0
      bandwidth_viz_test/perlin.js
  4. +1196
    -0
      bandwidth_viz_test/tinycolor.js

+ 23
- 0
bandwidth_viz_test/crc32.js View File

@ -0,0 +1,23 @@
var makeCRCTable = function(){
var c;
var crcTable = [];
for(var n =0; n < 256; n++){
c = n;
for(var k =0; k < 8; k++){
c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
}
crcTable[n] = c;
}
return crcTable;
}
var crc32 = function(str) {
var crcTable = window.crcTable || (window.crcTable = makeCRCTable());
var crc = 0 ^ (-1);
for (var i = 0; i < str.length; i++ ) {
crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
}
return (crc ^ (-1)) >>> 0;
};

+ 536
- 0
bandwidth_viz_test/index.html View File

@ -0,0 +1,536 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>greenhouse</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.nodes {
height: 600px;
background-color: aquamarine;
border: 3px dashed blue;
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: flex-start;
align-content: flex-start;
}
.node {
width: 100px;
height: 100px;
margin: 10px;
padding: 10px;
background-color: rgb(86, 202, 183);
border: 1px solid rgb(0, 63, 114);
display: flex;
flex-direction: column;
box-sizing: content-box;
}
.nameplate {
background-color: white;
color: #333;
margin-bottom: 5px;
font-size: 11px;
}
.node-content {
display: flex;
flex-direction: row;
justify-content: flex-end;
flex-grow: 1;
box-sizing: content-box;
max-height: calc(100% - 20px);
}
.current-tenants, .pinned-tenants {
display: inline-flex;
writing-mode: vertical-lr;
flex-wrap: wrap;
align-content: flex-start;
margin-right: 5px;
padding: 2px;
min-width: 10px;
min-height: 100%;
}
.current-tenants {
border: 1px solid greenyellow;
}
.pinned-tenants {
border: 1px dashed gray;
}
.tenant {
writing-mode: horizontal-tb;
color: white;
border-radius: 5px;
padding: 3px;
font-size: 10px;
}
.thermometer {
background-color: gray;
min-height: 100%;
width: 10px;
padding: 2px;
margin-right: 5px;
display: flex;
flex-direction: column-reverse;
}
.thermometer.projected {
margin-right: 0;
}
.marker {
display: block;
width: 8px;
}
</style>
<script src="crc32.js"></script>
<script src="perlin.js"></script>
<script src="tinycolor.js"></script>
<script>
const spikeMin = 10000000;
const spikeMax = 30000000000;
const spikePow = 2;
const instanceMonthlyBytes = 1000000000000;
const spikeRarityMin = 1;
const spikeRarityMax = 3;
const spikeRarityPow = 0.5;
const perlinIntervalMs = 100;
const perlinThreshold = 0.5;
const populationIntervalMs = 1000;
const monthInSeconds = 300;
const decomissionAfterInSeconds = 3;
const newTenantLikelyhood = 0.1;
const nodeSpawningInertiaFudgeFactor = 1.2;
const nodeTerminatingInertiaFudgeFactor = 1.2;
const rebalanceInertiaFudgeFactor = 1.1;
//----
const goldenRatio = 1.61803;
const nonCorrelatedSineFudgeFactor = 0.6934;
let tenants = [];
let nodes = [];
let endOfMonth = 0;
let amountOfMonthElapsed = 0;
let tenantNode = {};
const randRangePow = (min, max, pow) => min + (max * Math.pow(Math.random(), pow))
const secondsNow = () => (new Date()).getTime()*0.001;
const clamp = (v, min, max) => v > max ? max : (v < min ? min : v);
const abs = (v) => v > 0 ? v : -v;
const max = (a,b) => a > b ? a : b;
const lerp = (a, b, l) => { l = l > 1 ? 1 : (l < 0 ? 0 : l); return (a*(1-l))+b*l; }
const hsvLerp = (a, b, l) => ({
h: lerp(a.h, b.h, l),
s: lerp(a.s, b.s, l),
v: lerp(a.v, b.v, l),
});
const offsetCache = {};
const getOffset = (id, sub) => {
const str = `${id}_${sub||0}`;
if(!offsetCache[str]) {
offsetCache[str] = crc32(str)*0.0001;
}
return offsetCache[str];
};
const normPerlin = (id, freq, amp, pow, threshold) => {
threshold = threshold||0;
let signals = [];
if(typeof freq == 'number') {
signals = [perlin.get(0, getOffset(id)+secondsNow()*freq)]
} else if(freq.length != undefined) {
signals = freq.map((x, i) => perlin.get(0, getOffset(id, i)+secondsNow()*x))
}
return Math.pow((clamp(signals.reduce((a,b) => a+b, 0)+0.5, threshold, 1)-threshold)/(1-threshold), pow||1)*(amp||1);
};
let tenantIdSerial = 1;
const addTenant = () => {
const id = tenantIdSerial++;
const color = {
h: (goldenRatio * id * 360) % 360,
s: 0.7 + Math.sin(nonCorrelatedSineFudgeFactor*id)*0.3,
v: 0.5 + Math.sin(nonCorrelatedSineFudgeFactor*goldenRatio*id)*0.3,
};
tenants.push({
id: id,
color: color,
spikeSize: randRangePow(spikeMin, spikeMax, spikePow),
spikeRarity: randRangePow(spikeRarityMin, spikeRarityMax, spikeRarityPow),
billingBytes: 0,
currentNodes: [],
pinnedNodes: [],
})
console.log(`spawn tenant_${tenants[tenants.length-1].id}`)
return tenants[tenants.length-1];
};
let nodeIdSerial = 1;
const addNode = () => {
const container = document.querySelector(".nodes");
const element = document.createElement("div");
element.className = "node";
const id = nodeIdSerial++;
const nameplate = document.createElement("div");
nameplate.className = "nameplate";
nameplate.textContent = `node_${id}`;
element.id = `node_${id}`;
const contentContainer = document.createElement("div");
contentContainer.className = "node-content";
const currentTenantsContainer = document.createElement("div");
currentTenantsContainer.className = "current-tenants";
const pinnedTenantsContainer = document.createElement("div");
pinnedTenantsContainer.className = "pinned-tenants";
const realBytesContainer = document.createElement("div");
realBytesContainer.className = "thermometer real";
const real1 = document.createElement("div");
real1.className = "marker real-1";
const real2 = document.createElement("div");
real2.className = "marker real-2";
const projectedBytesContainer = document.createElement("div");
projectedBytesContainer.className = "thermometer projected";
const projected1 = document.createElement("div");
projected1.className = "marker projected-1";
const projected2 = document.createElement("div");
projected2.className = "marker projected-2";
element.appendChild(nameplate);
contentContainer.appendChild(currentTenantsContainer);
contentContainer.appendChild(pinnedTenantsContainer);
realBytesContainer.appendChild(real1);
realBytesContainer.appendChild(real2);
contentContainer.appendChild(realBytesContainer);
projectedBytesContainer.appendChild(projected1);
projectedBytesContainer.appendChild(projected2);
contentContainer.appendChild(projectedBytesContainer);
element.appendChild(contentContainer);
container.appendChild(element);
nodes.push({
id: id,
usedBytes: 0,
allowanceBytes: 0,
monthlyBytes: instanceMonthlyBytes,
currentTenants: [],
element: element
})
};
const decomissionNode = (node) => {
document.getElementById(`node_${node.id}`).classList.add("decomissioned");
node.decomissionAfter = secondsNow() + decomissionAfterInSeconds;
forEachTenantOnNode(node, scheduleTenant);
};
const removeNode = (node) => {
document.querySelector(".nodes").removeChild(document.getElementById(`node_${node.id}`));
tenants.forEach(tenant => {
tenant.pinnedNodes = tenant.pinnedNodes.filter(y => y.id != node.id)
});
nodes = nodes.filter(x => x.id != node.id);
};
const scheduleTenant = (tenant) => {
if(nodes.length == 0) {
addNode();
}
let largestSurplusNode = nodes[0];
let largestSurplus = 0;
nodes.forEach(node => {
const currentSurplus = node.monthlyBytes - getTotalProjectedUsage(node);
if(!node.decomissionAfter && currentSurplus > largestSurplus) {
largestSurplusNode = node;
largestSurplus = currentSurplus;
}
});
setTenantToNode(tenant, largestSurplusNode);
};
const setTenantToNode = (tenant, node) => {
const addTenantAsChildTo = (parent) => {
if(parent) {
const element = document.createElement("span");
element.className = `tenant tenant_${tenant.id}`;
element.textContent = tenant.id;
element.style.backgroundColor = tinycolor(tenant.color).toRgbString();
parent.appendChild(element);
}
};
const toPin = tenant.currentNodes.filter(x => x.id != node.id);
addTenantAsChildTo( document.querySelector(`#node_${node.id} .current-tenants`) );
tenant.currentNodes = [node];
// When a tenant leaves a current node, add it to the pinned nodes for a few hours
// to let connections and stale dns caches drain
toPin.forEach(nodeToPin => {
const existingCurrentNodeElement = document.querySelector(`#node_${nodeToPin.id} .current-tenants .tenant_${tenant.id}`);
if(existingCurrentNodeElement) {
existingCurrentNodeElement.parentElement.removeChild(existingCurrentNodeElement);
}
if(tenant.pinnedNodes.filter(x => x.id == nodeToPin.id).length == 0) {
addTenantAsChildTo( document.querySelector(`#node_${nodeToPin.id} .pinned-tenants`) );
tenant.pinnedNodes.push(nodeToPin);
setTimeout(() => {
console.log(`removing pinned tenant_${tenant.id} from node_${nodeToPin.id}`)
if(tenant.currentNodes.filter(x => x.id == nodeToPin.id).length == 0) {
tenant.pinnedNodes = tenant.pinnedNodes.filter(x => x.id != nodeToPin.id);
const pinnedNodeElement = document.querySelector(`#node_${nodeToPin.id} .pinned-tenants .tenant_${tenant.id}`);
if(pinnedNodeElement) {
pinnedNodeElement.parentElement.removeChild(pinnedNodeElement);
}
}
}, decomissionAfterInSeconds*1000);
}
});
};
const forEachTenantOnNode = (node, action) => {
tenants.forEach(tenant => {
if(tenant.currentNodes.filter(x => x.id == node.id).length ) {
action(tenant);
}
})
};
const getTotalProjectedUsage = (node) => {
let usageFromCurrentTenants = 0;
forEachTenantOnNode(node, (tenant) => {
usageFromCurrentTenants += tenant.billingBytes/tenant.currentNodes.length;
});
const projectedFutureUsage = (usageFromCurrentTenants / amountOfMonthElapsed)-usageFromCurrentTenants;
return node.usedBytes+projectedFutureUsage;
};
endOfMonth = secondsNow()+monthInSeconds;
window.addEventListener('DOMContentLoaded', () => {
scheduleTenant(addTenant());
scheduleTenant(addTenant());
scheduleTenant(addTenant());
scheduleTenant(addTenant());
scheduleTenant(addTenant());
});
setInterval(() => {
if(Math.random() < newTenantLikelyhood) {
scheduleTenant(addTenant());
}
amountOfMonthElapsed = 1-( (endOfMonth-secondsNow())/monthInSeconds );
// STEP 1: Adjust # of nodes up or down based on total projected usage for this month.
let totalProjectedUsage = 0;
let totalMonthlyAllowance = 0;
let atLeastOneNodeUsedUpPortionOfMonthly = false;
nodes.forEach(node => {
if(node.usedBytes > node.monthlyBytes*0.25) {
atLeastOneNodeUsedUpPortionOfMonthly = true;
}
if(!node.decomissionAfter) {
totalProjectedUsage += node.usedBytes/amountOfMonthElapsed;
totalMonthlyAllowance += node.monthlyBytes;
}
});
// dont adjust # of nodes right away at beginning of month... otherwise chaos might ensue!
if(amountOfMonthElapsed > 0.1 || atLeastOneNodeUsedUpPortionOfMonthly) {
while(totalProjectedUsage > (totalMonthlyAllowance+instanceMonthlyBytes*nodeSpawningInertiaFudgeFactor)) {
addNode();
totalMonthlyAllowance += instanceMonthlyBytes;
}
while(nodes.filter(x => !x.decomissionAfter).length > 1 && totalProjectedUsage < (totalMonthlyAllowance-instanceMonthlyBytes*nodeTerminatingInertiaFudgeFactor)) {
let nodeWithMostPerfectUtilization = null;
let mostPerfectUtilization = instanceMonthlyBytes;
nodes.forEach(node => {
const utilization = abs(node.usedBytes - node.allowanceBytes);
if(!node.decomissionAfter && (!nodeWithMostPerfectUtilization || utilization < mostPerfectUtilization)) {
mostPerfectUtilization = utilization;
nodeWithMostPerfectUtilization = node;
}
});
if(!nodeWithMostPerfectUtilization) {
break;
}
decomissionNode(nodeWithMostPerfectUtilization);
setTimeout(() => removeNode(nodeWithMostPerfectUtilization), decomissionAfterInSeconds*1000);
}
}
const getSortedNodes = () => {
let sorted = [];
nodes.forEach(x => sorted.push(x));
let cache = {};
sorted.sort((a, b) => {
const usageA = cache[a.id] || (cache[a.id] = getTotalProjectedUsage(a));
const usageB = cache[a.id] || (cache[a.id] = getTotalProjectedUsage(a));
return usageA - usageB;
});
return sorted;
}
//console.log(getSortedNodes().map(x => `${x.id}: ${getTotalProjectedUsage(x)}`).join("\n"))
let balanced = false;
let iterations = 0;
const cache = {};
//const hasTenantsCache = {};
while(!balanced && iterations < 100) {
iterations++;
let smallestProjectedUsage = instanceMonthlyBytes*10000;
let smallestProjectedUsageNode = null;
let largestProjectedUsage = 0;
let largestProjectedUsageNode = null;
nodes.forEach(node => {
const projectedUsage = cache[node.id] || (cache[node.id] = getTotalProjectedUsage(node));
const hasTenants = tenants.filter(x => x.currentNodes.filter(y => y.id == node.id).length > 0).length > 0;
if(projectedUsage < smallestProjectedUsage) {
smallestProjectedUsageNode = node;
smallestProjectedUsage = projectedUsage;
}
if(projectedUsage > largestProjectedUsage && hasTenants) {
largestProjectedUsageNode = node;
largestProjectedUsage = projectedUsage;
}
});
let biggestTenant;
forEachTenantOnNode(largestProjectedUsageNode, tenant => {
if(tenant && (!biggestTenant || tenant.billingBytes > biggestTenant.billingBytes)) {
biggestTenant = tenant;
}
});
if(!biggestTenant) {
console.log(`error! biggestTenant is null!!`);
continue;
}
const biggestTenantProjectedUsage = biggestTenant.billingBytes/amountOfMonthElapsed;
if(largestProjectedUsage-smallestProjectedUsage > biggestTenantProjectedUsage*rebalanceInertiaFudgeFactor) {
delete cache[smallestProjectedUsageNode.id];
biggestTenant.currentNodes.forEach(x => {
delete cache[x.id];
});
setTenantToNode(biggestTenant, smallestProjectedUsageNode);
} else {
balanced = true;
return;
}
}
if(iterations >= 100) {
console.log(`error! iterations >= 100`);
}
if(secondsNow() >= endOfMonth) {
// Do billing
tenants.forEach(x => x.billingBytes = 0);
nodes.forEach(x => {
x.allowanceBytes = 0;
x.billingBytes = 0;
});
endOfMonth += monthInSeconds;
}
}, populationIntervalMs);
let lastPerlinTime = secondsNow();
setInterval(() => {
tenants.forEach((tenant, i) => {
const bytes = normPerlin(i, (0.1, 1), tenant.spikeSize, tenant.spikeRarity, perlinThreshold);
const destIndex = Math.floor(Math.random() * (tenant.pinnedNodes.length+tenant.currentNodes.length));
let destNode;
if(tenant.currentNodes.length > 0 && destIndex >= tenant.pinnedNodes.length) {
destNode = tenant.currentNodes[destIndex - tenant.pinnedNodes.length];
//console.log(`destIndex: ${destIndex} tenant.currentNodes[${destIndex - tenant.pinnedNodes.length}] len(${tenant.currentNodes.length})`);
} else {
destNode = tenant.pinnedNodes[destIndex];
//console.log(`destIndex: ${destIndex} tenant.pinnedNodes[${destIndex}] len(${tenant.pinnedNodes.length})`);
}
if(!destNode) {
return
}
const normalBackgroundColor = tinycolor(tenant.color).toRgbString();
Array.from(document.querySelectorAll(`.tenant_${tenant.id}`)).forEach(e => e.style.backgroundColor = normalBackgroundColor);
const destNodeTenant = document.querySelector(`#node_${destNode.id} .tenant_${tenant.id}`);
if(destNodeTenant) {
const white = { h: tenant.color.h, s: 0, v: 1 };
const lerp = bytes/tenant.spikeSize;
destNodeTenant.style.backgroundColor = tinycolor(hsvLerp(tenant.color, white, lerp)).toRgbString();
}
tenant.billingBytes += bytes;
destNode.usedBytes += bytes;
});
const now = secondsNow();
const elapsedSeconds = now - lastPerlinTime;
lastPerlinTime = now;
nodes.forEach(node => {
node.allowanceBytes += node.monthlyBytes*(elapsedSeconds/monthInSeconds);
const real1 = document.querySelector(`#node_${node.id} .real-1`);
const real2 = document.querySelector(`#node_${node.id} .real-2`);
const projected1 = document.querySelector(`#node_${node.id} .projected-1`);
const projected2 = document.querySelector(`#node_${node.id} .projected-2`);
if(real1 && real2 && projected1 && projected2) {
const usage = (getTotalProjectedUsage(node)/node.monthlyBytes)*0.5;
//console.log(`used: ${node.usedBytes} .. ${node.allowanceBytes} .. ${node.monthlyBytes}`)
if(node.allowanceBytes > node.usedBytes) {
real1.style.backgroundColor = "cyan";
real2.style.backgroundColor = "#ddd";
real1.style.minHeight = `${(node.usedBytes/node.monthlyBytes)*100}%`;
real2.style.minHeight = `${((node.allowanceBytes-node.usedBytes)/node.monthlyBytes)*100}%`;
} else {
real1.style.backgroundColor = "cyan";
real2.style.backgroundColor = "orange";
real1.style.minHeight = `${(node.allowanceBytes/node.monthlyBytes)*100}%`;
real2.style.minHeight = `${((node.allowanceBytes-node.usedBytes)/node.monthlyBytes)*100}%`;
}
if(usage < 0.5) {
projected1.style.backgroundColor = "green";
projected2.style.backgroundColor = "#ddd";
projected1.style.minHeight = `${usage*100}%`;
projected2.style.minHeight = `${(0.5-usage)*100}%`;
} else {
projected1.style.backgroundColor = "green";
projected2.style.backgroundColor = "orange";
projected1.style.minHeight = `50%`;
projected2.style.minHeight = `${(usage-0.5)*100}%`;
}
}
});
}, perlinIntervalMs);
</script>
</head>
<body>
<div class="nodes">
</div>
<div class="timeline">
</div>
</body>
</html>

+ 45
- 0
bandwidth_viz_test/perlin.js View File

@ -0,0 +1,45 @@
'use strict';
let perlin = {
rand_vect: function(){
let theta = Math.random() * 2 * Math.PI;
return {x: Math.cos(theta), y: Math.sin(theta)};
},
dot_prod_grid: function(x, y, vx, vy){
let g_vect;
let d_vect = {x: x - vx, y: y - vy};
if (this.gradients[[vx,vy]]){
g_vect = this.gradients[[vx,vy]];
} else {
g_vect = this.rand_vect();
this.gradients[[vx, vy]] = g_vect;
}
return d_vect.x * g_vect.x + d_vect.y * g_vect.y;
},
smootherstep: function(x){
return 6*x**5 - 15*x**4 + 10*x**3;
},
interp: function(x, a, b){
return a + this.smootherstep(x) * (b-a);
},
seed: function(){
this.gradients = {};
this.memory = {};
},
get: function(x, y) {
if (this.memory.hasOwnProperty([x,y]))
return this.memory[[x,y]];
let xf = Math.floor(x);
let yf = Math.floor(y);
//interpolate
let tl = this.dot_prod_grid(x, y, xf, yf);
let tr = this.dot_prod_grid(x, y, xf+1, yf);
let bl = this.dot_prod_grid(x, y, xf, yf+1);
let br = this.dot_prod_grid(x, y, xf+1, yf+1);
let xt = this.interp(x-xf, tl, tr);
let xb = this.interp(x-xf, bl, br);
let v = this.interp(y-yf, xt, xb);
this.memory[[x,y]] = v;
return v;
}
}
perlin.seed();

+ 1196
- 0
bandwidth_viz_test/tinycolor.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save