<!-- TCP CUBIC vs Reno congestion-window simulation -->
<div class="controls">
<label class="ctrl-label">RTT: <input id="rtt" type="range" min="10" max="200" value="50" step="10"> <span id="rtt-val">50 ms</span></label>
<label class="ctrl-label">Bandwidth: <input id="bw" type="range" min="10" max="200" value="100" step="10"> <span id="bw-val">100 Mbps</span></label>
</div>
<canvas id="chart" width="560" height="260"></canvas>
<div class="legend">
<span class="dot cubic"></span> CUBIC
<span class="dot reno"></span> Reno
</div>
<div class="status" id="status">Press "Simulate loss" to start both algorithms and trigger a loss event.</div>
<div class="btns">
<button id="btn-loss" type="button">Simulate loss</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the CUBIC vs Reno congestion window chart */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; margin-bottom: .6rem; font-size: .85rem; }
.ctrl-label { display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 100px; accent-color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafb; max-width: 100%; }
.legend { font-size: .82rem; margin: .4rem 0; display: flex; gap: 1rem; align-items: center; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
.dot.cubic { background: #1d6fa5; }
.dot.reno { background: #e06c00; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* TCP CUBIC vs Reno side-by-side simulation */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Simulation constants */
const C_CUBIC = 0.4; // CUBIC scaling constant (Linux default 0.4)
const BETA = 0.3; // multiplicative decrease factor
const MSS = 1500; // maximum segment size in bytes
const SIM_SEC = 8; // total simulation length in seconds
const STEP = 0.04; // time step per animation frame in seconds
let rttMs = 50;
let bwMbps = 100;
let cubicData = [];
let renoData = [];
let lossTime = null; // wall-clock second when loss event was triggered
let running = false;
let animId = null;
let tSim = 0;
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');
function setStatus(msg) { statusEl.textContent = msg; }
/* CUBIC window at elapsed time t since loss, given wMax at loss */
function cubicWindow(t, wMax) {
const K = Math.cbrt((wMax * BETA) / C_CUBIC);
return C_CUBIC * Math.pow(t - K, 3) + wMax;
}
/* Reno window grows linearly at 1 segment per RTT */
function renoWindow(t, wMax, rtt) {
return (wMax / 2) + (t / rtt);
}
function maxSegs() {
/* target max window = 2x the bandwidth-delay product in segments */
return Math.max(200, Math.round((bwMbps * 1e6 / 8 * rttMs / 1000) / MSS) * 2);
}
function draw() {
const W = canvas.width;
const H = canvas.height;
ctx.clearRect(0, 0, W, H);
const pad = { top: 20, right: 20, bottom: 36, left: 56 };
const pw = W - pad.left - pad.right;
const ph = H - pad.top - pad.bottom;
const maxW = maxSegs();
/* draw chart axes */
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pad.left, pad.top);
ctx.lineTo(pad.left, pad.top + ph);
ctx.lineTo(pad.left + pw, pad.top + ph);
ctx.stroke();
/* draw tick marks and labels */
ctx.fillStyle = '#888';
ctx.font = '11px system-ui';
ctx.textAlign = 'right';
for (let segs of [0, Math.round(maxW * 0.25), Math.round(maxW * 0.5), Math.round(maxW * 0.75), maxW]) {
const y = pad.top + ph - (segs / maxW) * ph;
ctx.beginPath(); ctx.moveTo(pad.left - 4, y); ctx.lineTo(pad.left, y); ctx.stroke();
ctx.fillText(segs, pad.left - 7, y + 4);
}
ctx.textAlign = 'center';
for (let s = 0; s <= SIM_SEC; s += 2) {
const x = pad.left + (s / SIM_SEC) * pw;
ctx.beginPath(); ctx.moveTo(x, pad.top + ph); ctx.lineTo(x, pad.top + ph + 4); ctx.stroke();
ctx.fillText(s + 's', x, pad.top + ph + 15);
}
/* axis labels */
ctx.save();
ctx.translate(14, pad.top + ph / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillText('Window (segments)', 0, 0);
ctx.restore();
ctx.textAlign = 'center';
ctx.fillText('Time (seconds)', pad.left + pw / 2, H - 4);
/* draw vertical line at the loss event */
if (lossTime !== null) {
const lx = pad.left + (lossTime / SIM_SEC) * pw;
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(lx, pad.top);
ctx.lineTo(lx, pad.top + ph);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#c0392b';
ctx.textAlign = 'left';
ctx.font = '10px system-ui';
ctx.fillText('loss', lx + 3, pad.top + 12);
}
/* draw one data series as a polyline */
function drawSeries(data, color) {
if (data.length < 2) return;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
data.forEach(([t, w], i) => {
const x = pad.left + (t / SIM_SEC) * pw;
const y = pad.top + ph - Math.min(w, maxW) / maxW * ph;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
drawSeries(cubicData, '#1d6fa5');
drawSeries(renoData, '#e06c00');
}
function simulate() {
/* advance simulation by one step */
if (lossTime === null) {
/* before a loss event, both algorithms hold steady at 80% wMax */
const wNow = maxSegs() * 0.8;
cubicData.push([tSim, wNow]);
renoData.push([tSim, wNow]);
draw();
tSim += STEP;
if (tSim < SIM_SEC) animId = requestAnimationFrame(simulate);
else { running = false; setStatus('Simulation complete.'); }
return;
}
const elapsed = tSim - lossTime;
const wMax = maxSegs() * 0.8; // window at the moment of loss; fixed so both algorithms start equal
const wCubic = Math.max(1, cubicWindow(elapsed, wMax));
const rttSec = rttMs / 1000;
const wReno = Math.max(1, renoWindow(elapsed, wMax, rttSec));
cubicData.push([tSim, wCubic]);
renoData.push([tSim, wReno]);
draw();
tSim += STEP;
if (tSim < SIM_SEC) {
animId = requestAnimationFrame(simulate);
} else {
running = false;
setStatus(i18nFmt('Final window — CUBIC: %cubic% segs, Reno: %reno% segs.', {
cubic: Math.round(wCubic),
reno: Math.round(wReno)
}));
}
}
function triggerLoss() {
if (lossTime !== null) return;
lossTime = tSim;
setStatus('Loss triggered! Watch CUBIC recover faster than Reno.');
}
function reset() {
if (animId) cancelAnimationFrame(animId);
running = false;
lossTime = null;
tSim = 0;
cubicData = [];
renoData = [];
setStatus('Press "Simulate loss" to start both algorithms and trigger a loss event.');
draw();
}
document.getElementById('rtt').addEventListener('input', function() {
rttMs = +this.value;
document.getElementById('rtt-val').textContent = rttMs + ' ms';
});
document.getElementById('bw').addEventListener('input', function() {
bwMbps = +this.value;
document.getElementById('bw-val').textContent = bwMbps + ' Mbps';
});
document.getElementById('btn-loss').addEventListener('click', function() {
if (!running) {
running = true;
setStatus('Running — press "Simulate loss" again to trigger loss now.');
simulate();
setTimeout(triggerLoss, 2000);
} else {
triggerLoss();
}
});
document.getElementById('btn-reset').addEventListener('click', reset);
draw();
<!-- TCP CUBIC vs Reno congestion-window simulation -->
<div class="controls">
<label class="ctrl-label">RTT: <input id="rtt" type="range" min="10" max="200" value="50" step="10"> <span id="rtt-val">50 ms</span></label>
<label class="ctrl-label">Bandwidth: <input id="bw" type="range" min="10" max="200" value="100" step="10"> <span id="bw-val">100 Mbps</span></label>
</div>
<canvas id="chart" width="560" height="260"></canvas>
<div class="legend">
<span class="dot cubic"></span> CUBIC
<span class="dot reno"></span> Reno
</div>
<div class="status" id="status">Press "Simulate loss" to start both algorithms and trigger a loss event.</div>
<div class="btns">
<button id="btn-loss" type="button">Simulate loss</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the CUBIC vs Reno congestion window chart */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; margin-bottom: .6rem; font-size: .85rem; }
.ctrl-label { display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 100px; accent-color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafb; max-width: 100%; }
.legend { font-size: .82rem; margin: .4rem 0; display: flex; gap: 1rem; align-items: center; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
.dot.cubic { background: #1d6fa5; }
.dot.reno { background: #e06c00; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* TCP CUBIC vs Reno side-by-side simulation */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Simulation constants */
const C_CUBIC = 0.4; // CUBIC scaling constant (Linux default 0.4)
const BETA = 0.3; // multiplicative decrease factor
const MSS = 1500; // maximum segment size in bytes
const SIM_SEC = 8; // total simulation length in seconds
const STEP = 0.04; // time step per animation frame in seconds
let rttMs = 50;
let bwMbps = 100;
let cubicData = [];
let renoData = [];
let lossTime = null; // wall-clock second when loss event was triggered
let running = false;
let animId = null;
let tSim = 0;
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');
function setStatus(msg) { statusEl.textContent = msg; }
/* CUBIC window at elapsed time t since loss, given wMax at loss */
function cubicWindow(t, wMax) {
const K = Math.cbrt((wMax * BETA) / C_CUBIC);
return C_CUBIC * Math.pow(t - K, 3) + wMax;
}
/* Reno window grows linearly at 1 segment per RTT */
function renoWindow(t, wMax, rtt) {
return (wMax / 2) + (t / rtt);
}
function maxSegs() {
/* target max window = 2x the bandwidth-delay product in segments */
return Math.max(200, Math.round((bwMbps * 1e6 / 8 * rttMs / 1000) / MSS) * 2);
}
function draw() {
const W = canvas.width;
const H = canvas.height;
ctx.clearRect(0, 0, W, H);
const pad = { top: 20, right: 20, bottom: 36, left: 56 };
const pw = W - pad.left - pad.right;
const ph = H - pad.top - pad.bottom;
const maxW = maxSegs();
/* draw chart axes */
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pad.left, pad.top);
ctx.lineTo(pad.left, pad.top + ph);
ctx.lineTo(pad.left + pw, pad.top + ph);
ctx.stroke();
/* draw tick marks and labels */
ctx.fillStyle = '#888';
ctx.font = '11px system-ui';
ctx.textAlign = 'right';
for (let segs of [0, Math.round(maxW * 0.25), Math.round(maxW * 0.5), Math.round(maxW * 0.75), maxW]) {
const y = pad.top + ph - (segs / maxW) * ph;
ctx.beginPath(); ctx.moveTo(pad.left - 4, y); ctx.lineTo(pad.left, y); ctx.stroke();
ctx.fillText(segs, pad.left - 7, y + 4);
}
ctx.textAlign = 'center';
for (let s = 0; s <= SIM_SEC; s += 2) {
const x = pad.left + (s / SIM_SEC) * pw;
ctx.beginPath(); ctx.moveTo(x, pad.top + ph); ctx.lineTo(x, pad.top + ph + 4); ctx.stroke();
ctx.fillText(s + 's', x, pad.top + ph + 15);
}
/* axis labels */
ctx.save();
ctx.translate(14, pad.top + ph / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillText('Window (segments)', 0, 0);
ctx.restore();
ctx.textAlign = 'center';
ctx.fillText('Time (seconds)', pad.left + pw / 2, H - 4);
/* draw vertical line at the loss event */
if (lossTime !== null) {
const lx = pad.left + (lossTime / SIM_SEC) * pw;
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(lx, pad.top);
ctx.lineTo(lx, pad.top + ph);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#c0392b';
ctx.textAlign = 'left';
ctx.font = '10px system-ui';
ctx.fillText('loss', lx + 3, pad.top + 12);
}
/* draw one data series as a polyline */
function drawSeries(data, color) {
if (data.length < 2) return;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
data.forEach(([t, w], i) => {
const x = pad.left + (t / SIM_SEC) * pw;
const y = pad.top + ph - Math.min(w, maxW) / maxW * ph;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
drawSeries(cubicData, '#1d6fa5');
drawSeries(renoData, '#e06c00');
}
function simulate() {
/* advance simulation by one step */
if (lossTime === null) {
/* before a loss event, both algorithms hold steady at 80% wMax */
const wNow = maxSegs() * 0.8;
cubicData.push([tSim, wNow]);
renoData.push([tSim, wNow]);
draw();
tSim += STEP;
if (tSim < SIM_SEC) animId = requestAnimationFrame(simulate);
else { running = false; setStatus('Simulation complete.'); }
return;
}
const elapsed = tSim - lossTime;
const wMax = maxSegs() * 0.8; // window at the moment of loss; fixed so both algorithms start equal
const wCubic = Math.max(1, cubicWindow(elapsed, wMax));
const rttSec = rttMs / 1000;
const wReno = Math.max(1, renoWindow(elapsed, wMax, rttSec));
cubicData.push([tSim, wCubic]);
renoData.push([tSim, wReno]);
draw();
tSim += STEP;
if (tSim < SIM_SEC) {
animId = requestAnimationFrame(simulate);
} else {
running = false;
setStatus(i18nFmt('Final window — CUBIC: %cubic% segs, Reno: %reno% segs.', {
cubic: Math.round(wCubic),
reno: Math.round(wReno)
}));
}
}
function triggerLoss() {
if (lossTime !== null) return;
lossTime = tSim;
setStatus('Loss triggered! Watch CUBIC recover faster than Reno.');
}
function reset() {
if (animId) cancelAnimationFrame(animId);
running = false;
lossTime = null;
tSim = 0;
cubicData = [];
renoData = [];
setStatus('Press "Simulate loss" to start both algorithms and trigger a loss event.');
draw();
}
document.getElementById('rtt').addEventListener('input', function() {
rttMs = +this.value;
document.getElementById('rtt-val').textContent = rttMs + ' ms';
});
document.getElementById('bw').addEventListener('input', function() {
bwMbps = +this.value;
document.getElementById('bw-val').textContent = bwMbps + ' Mbps';
});
document.getElementById('btn-loss').addEventListener('click', function() {
if (!running) {
running = true;
setStatus('Running — press "Simulate loss" again to trigger loss now.');
simulate();
setTimeout(triggerLoss, 2000);
} else {
triggerLoss();
}
});
document.getElementById('btn-reset').addEventListener('click', reset);
draw();
Notice that Reno climbs at a constant one-segment-per-RTT slope, while CUBIC rockets past and then flattens as it approaches the old maximum — probing carefully right where another loss is most likely. On high-bandwidth, high-latency paths the difference in throughput can be dramatic.
Comments
Loading comments...