Last active
January 29, 2025 18:08
-
-
Save gusost/79948c1c9ed4e9348a22bb71ea20d54a to your computer and use it in GitHub Desktop.
Offline re-connection simulation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Backoff Strategy Visualization</title> | |
<script src="https://d3js.org/d3.v7.min.js"></script> | |
<style> | |
body { | |
font-family: Helvetica, sans-serif; | |
} | |
.bar { | |
fill: steelblue; | |
} | |
.axis-label { | |
font-size: 14px; | |
} | |
</style> | |
</head> | |
<body> | |
<h2>Backoff Strategy Visualization</h2> | |
<label>Calls: <input type="number" id="calls" value="100000"></label> | |
<label>Failure Rate (0-1): <input type="number" step="0.01" id="failureRate" value="1"></label> | |
<label>Jitter Factor: <input type="number" step="0.1" id="jitter" value="10"></label> | |
<label>Min Backoff (ms): <input type="number" id="minBackoff" value="20000"></label> | |
<label>Max Backoff (ms): <input type="number" id="maxBackoff" value="60000"></label> | |
<label>Max server latency (ms): <input type="number" id="maxServerLatency" value="15000"></label> | |
<button onclick="updateUIinputGraph()">Run Simulation</button> | |
<div> | |
<svg id="ui-input" width="750" height="300"></svg> | |
<svg id="ui-input-expo" width="750" height="300"></svg> | |
<svg id="blip" width="750" height="300"></svg> | |
<svg id="half" width="750" height="300"></svg> | |
<svg id="outage" width="750" height="300"></svg> | |
</div> | |
<script> | |
const maxRetryPlotTime = 60 * 60 * 1000 // 1 hour | |
let calls = parseInt(document.getElementById('calls').value) | |
let maxServerLatency = parseInt(document.getElementById('maxServerLatency').value) | |
const transitionTime = 0 // animation time in ms | |
const jitter = 1 | |
const minBackoff = 60e3 | |
const maxBackoff = 600e3 | |
const variant = `Exponetial backoff, jitter ${jitter}, min ${minBackoff / 1000}s, max ${maxBackoff / 1000}s` | |
//const reqDelay = productionOriginalBackoff(jitter, minBackoff, maxBackoff) | |
const reqDelay = exponentialBackoff(jitter, minBackoff, maxBackoff) | |
function productionOriginalBackoff(jitter, minBackoff, maxBackoff) { | |
return function reqDelay(attempt) { | |
let delay = Math.min(fib(attempt) * minBackoff, maxBackoff) | |
delay += delay * jitter * Math.random() | |
return delay | |
} | |
} | |
function exponentialBackoff(jitter, minBackoff, maxBackoff) { | |
return function reqDelay(attempt) { | |
let delay = Math.min(minBackoff * Math.pow(2, attempt - 1), maxBackoff) | |
let jitterAmount = jitter * delay * (Math.random() * 2 - 1) | |
return Math.max(0, delay + jitterAmount) | |
} | |
} | |
function simulateBlip() { | |
let failureRate = 0.05 | |
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay }) | |
} | |
function simulateHalf() { | |
let failureRate = 0.5 | |
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay }) | |
} | |
function simulateOutage() { | |
let failureRate = 1 | |
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay }) | |
} | |
function simulateRequestsUsingUIinput() { | |
let calls = parseInt(document.getElementById('calls').value) | |
let failureRate = parseFloat(document.getElementById('failureRate').value) | |
let jitter = parseFloat(document.getElementById('jitter').value) | |
let minBackoff = parseInt(document.getElementById('minBackoff').value) | |
let maxBackoff = parseInt(document.getElementById('maxBackoff').value) | |
let maxServerLatency = parseInt(document.getElementById('maxServerLatency').value) | |
let reqDelay = productionOriginalBackoff(jitter, minBackoff, maxBackoff) | |
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay }) | |
} | |
function simulateRequestsUsingUIinputExpo() { | |
let calls = parseInt(document.getElementById('calls').value) | |
let failureRate = parseFloat(document.getElementById('failureRate').value) | |
let jitter = parseFloat(document.getElementById('jitter').value) | |
let minBackoff = parseInt(document.getElementById('minBackoff').value) | |
let maxBackoff = parseInt(document.getElementById('maxBackoff').value) | |
let maxServerLatency = parseInt(document.getElementById('maxServerLatency').value) | |
const reqDelay = exponentialBackoff(jitter, minBackoff, maxBackoff) | |
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay }) | |
} | |
function simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay }) { | |
let requestTimes = [] | |
let now = 0 | |
for (let i = 0; i < calls; i++) { | |
let attempt = 1 | |
let connectDelay = reqDelay(attempt) | |
requestTimes.push(connectDelay) | |
while (Math.random() < failureRate && connectDelay < maxRetryPlotTime) { | |
connectDelay += reqDelay(++attempt) | |
// Fixed server latency. (all reconnection attempts are due to the same server timeout) | |
connectDelay += maxServerLatency | |
// Variable server latency. (reconnection attempts are due to server asking to back off and not due to timeout) | |
//connectDelay += Math.random() * maxServerLatency | |
requestTimes.push(connectDelay) | |
} | |
} | |
// Make request times seconds integers | |
requestTimes = requestTimes.map(t => Math.floor(t / 1000)) | |
let requestCounts = {} | |
requestTimes.forEach(t => requestCounts[t] = (requestCounts[t] || 0) + 1) | |
const plotArray = Object.entries(requestCounts).map(([t, count]) => ({ time: +t, count })) | |
// Count backwards and cut the array at a low sum count. This avoids a long tail of insignificant data in the graph | |
let sum = calls * 0.002 | |
for (let i = plotArray.length - 1; i >= 0; i--) { | |
sum -= plotArray[i].count | |
if (sum <= 0) { | |
plotArray.length = i + 1 | |
break | |
} | |
} | |
return plotArray | |
} | |
function updateUIinputGraph() { | |
let data = simulateRequestsUsingUIinput() | |
let svg = d3.select("svg#ui-input") | |
updateGraph(data, svg, "Production algo - UI Input") | |
let dataExpo = simulateRequestsUsingUIinputExpo() | |
let svgExpo = d3.select("svg#ui-input-expo") | |
updateGraph(dataExpo, svgExpo, "Exponetial backoff - UI Input") | |
} | |
function updateGraph(data, svg, title) { | |
let width = +svg.attr("width") | |
let height = +svg.attr("height") | |
let margin = { top: 20, right: 20, bottom: 20, left: 50 } | |
let innerWidth = width - margin.left - margin.right | |
let innerHeight = height - margin.top - margin.bottom | |
// Add title text to the top right corner | |
svg.selectAll(".title").data([null]) | |
.join("text") | |
.attr("class", "title") | |
.attr("x", width) | |
.attr("y", margin.top * 1.5) | |
.attr("text-anchor", "end") | |
.attr("font-size", "16px") | |
.text(title) | |
let x = d3.scaleLinear() | |
.domain([0, d3.max(data, d => d.time)]) | |
.range([0, innerWidth]) | |
let y = d3.scaleLinear() | |
.domain([0, d3.max(data, d => d.count)]) | |
.range([innerHeight, 0]) | |
let g = svg.selectAll("g").data([null]) | |
g = g.enter().append("g").merge(g) | |
.attr("transform", `translate(${margin.left},${margin.top})`) | |
let bars = g.selectAll("rect").data(data, d => d.time) | |
bars.enter().append("rect") | |
.merge(bars) | |
.transition().duration(transitionTime) | |
.attr("class", "bar") | |
.attr("x", d => x(d.time)) | |
.attr("width", innerWidth / data.length) | |
.attr("y", d => y(d.count)) | |
.attr("height", d => innerHeight - y(d.count)) | |
bars.exit().remove() | |
let xAxis = d3.axisBottom(x) | |
let yAxis = d3.axisLeft(y) | |
g.selectAll(".x-axis").data([null]) | |
.join("g") | |
.attr("class", "x-axis") | |
.attr("transform", `translate(0,${innerHeight})`) | |
.call(xAxis) | |
.append("text") | |
.attr("fill", "black") | |
.attr("x", innerWidth + margin.right) | |
.attr("y", 0) | |
.attr("text-anchor", "end") | |
.text("[s]") | |
g.selectAll(".y-axis").data([null]) | |
.join("g") | |
.attr("class", "y-axis") | |
.call(yAxis) | |
.append("text") | |
.attr("fill", "black") | |
.attr("x", -innerHeight / 2) | |
.attr("y", -40) | |
.attr("transform", "rotate(-90)") | |
.attr("text-anchor", "middle") | |
.text("[requests / s]") | |
} | |
updateUIinputGraph() | |
let blipSvg = d3.select("svg#blip") | |
let blipData = simulateBlip() | |
updateGraph(blipData, blipSvg, variant + " - 5% fail reconnect") | |
let halfSvg = d3.select("svg#half") | |
let halfData = simulateHalf() | |
updateGraph(halfData, halfSvg, variant + " - 50% fail reconnect") | |
let outageData = simulateOutage() | |
let outageSvg = d3.select("svg#outage") | |
updateGraph(outageData, outageSvg, variant + " - Outage") | |
// Linear time algoritm from https://wiki.c2.com/?FibonacciSequence | |
function fib(n) { | |
// This algortim starts of as 1, 1, 0, 1, 1, 2, 3, 5, etc. | |
// By adding two we can realign it to start as expected | |
n += 2 | |
let m = 1 | |
let k = 0 | |
for (let i = 1; i < n; i++) { | |
const tmp = m + k | |
m = k | |
k = tmp | |
} | |
return m | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment