The chart below simulates TCP Reno's AIMD loop. The congestion window (cwnd) starts small, grows quickly during slow start, then climbs linearly — until a packet loss event cuts it in half. Hit Run to watch the cycle repeat.
<!-- TCP Congestion Control demo — AIMD sawtooth simulator -->
<div class="controls">
<label>Loss prob: <input type="range" id="lossSlider" min="1" max="30" value="8"> <span id="lossVal">8%</span></label>
<label>Speed: <input type="range" id="speedSlider" min="1" max="10" value="5"> <span id="speedVal">5</span></label>
</div>
<canvas id="chart" width="560" height="220" aria-label="TCP congestion window over time"></canvas>
<div class="stats">
<span id="statCwnd">cwnd: 1</span>
<span id="statThroughput">avg cwnd: —</span>
<span id="statPhase">Phase: Slow Start</span>
</div>
<div class="btns">
<button id="btnRun" type="button">Run</button>
<button id="btnStep" type="button">Step</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="dot blue"></span>cwnd
<span class="dot red"></span>ssthresh
<span class="dot orange"></span>loss event
</div>
/* Base styles for the TCP congestion control demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; margin-bottom: .5rem; font-size: .85rem; }
.controls label { display: flex; align-items: center; gap: .4rem; }
canvas { display: block; width: 100%; max-width: 560px; border: 1px solid #d0d7de; border-radius: 8px; background: #fafbfc; }
.stats { display: flex; flex-wrap: wrap; gap: .5rem 1.2rem; margin: .4rem 0; font-size: .85rem; font-weight: 600; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.legend { display: flex; align-items: center; gap: .5rem; font-size: .78rem; color: #555; flex-wrap: wrap; margin-top: .3rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 2px; }
.dot.blue { background: #1d76c8; }
.dot.red { background: #c92f3c; }
.dot.orange { background: #e07b00; }
/* AIMD simulation — TCP Reno congestion control */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Simulation state variables */
let cwnd = 1;
let ssthresh = 16;
let phase = 'slow-start'; /* values: slow-start | congestion-avoidance */
let history = [];
let lossEvents = [];
let running = false;
let timer = null;
let totalAcks = 0;
let totalBytes = 0;
const MAX_TICKS = 80;
const MAX_CWND = 64;
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 12, right: 12, bottom: 28, left: 36 };
/* Read loss probability and speed from sliders */
function getLoss() { return parseInt(document.getElementById('lossSlider').value, 10) / 100; }
function getSpeed() { return parseInt(document.getElementById('speedSlider').value, 10); }
/* Reset all simulation state to initial values */
function resetState() {
cwnd = 1; ssthresh = 16; phase = 'slow-start';
history = []; lossEvents = [];
totalAcks = 0; totalBytes = 0;
running = false;
if (timer) { clearInterval(timer); timer = null; }
document.getElementById('btnRun').textContent = 'Run';
draw(); updateStats();
}
/* One RTT tick: decide loss, update window, record history */
function tick() {
const p = getLoss();
const lost = Math.random() < p;
if (lost) {
/* Loss detected: set ssthresh = cwnd/2, reset cwnd to 1 */
ssthresh = Math.max(Math.floor(cwnd / 2), 2);
cwnd = 1;
phase = 'slow-start';
lossEvents.push(history.length);
} else {
totalAcks++;
totalBytes += cwnd;
if (phase === 'slow-start') {
cwnd = Math.min(cwnd * 2, MAX_CWND); /* Slow start: double cwnd each RTT */
if (cwnd >= ssthresh) phase = 'congestion-avoidance';
} else {
cwnd = Math.min(cwnd + 1, MAX_CWND); /* Congestion avoidance: increase cwnd by 1 each RTT */
}
}
history.push({ cwnd, ssthresh });
/* Keep history bounded to MAX_TICKS entries */
if (history.length > MAX_TICKS) {
history.shift();
lossEvents = lossEvents.map(i => i - 1).filter(i => i >= 0);
}
draw(); updateStats();
}
/* Redraw the entire canvas chart */
function draw() {
ctx.clearRect(0, 0, W, H);
const cw = W - PAD.left - PAD.right;
const ch = H - PAD.top - PAD.bottom;
/* Draw horizontal grid lines at multiples of 8 */
ctx.strokeStyle = '#e0e4e8'; ctx.lineWidth = 0.5;
for (let g = 0; g <= MAX_CWND; g += 8) {
const y = PAD.top + ch - (g / MAX_CWND) * ch;
ctx.beginPath(); ctx.moveTo(PAD.left, y); ctx.lineTo(PAD.left + cw, y); ctx.stroke();
ctx.fillStyle = '#888'; ctx.font = '9px system-ui';
ctx.fillText(g, 2, y + 3);
}
if (history.length < 2) return;
const n = history.length;
const xOf = i => PAD.left + (i / (MAX_TICKS - 1)) * cw;
const yOf = v => PAD.top + ch - (Math.min(v, MAX_CWND) / MAX_CWND) * ch;
/* Draw ssthresh as a dashed red line */
ctx.strokeStyle = '#c92f3c'; ctx.lineWidth = 1; ctx.setLineDash([4, 3]);
ctx.beginPath();
history.forEach((h, i) => {
const x = xOf(i + (MAX_TICKS - n)), y = yOf(h.ssthresh);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke(); ctx.setLineDash([]);
/* Draw congestion window as a solid blue line */
ctx.strokeStyle = '#1d76c8'; ctx.lineWidth = 2;
ctx.beginPath();
history.forEach((h, i) => {
const x = xOf(i + (MAX_TICKS - n)), y = yOf(h.cwnd);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
/* Draw orange dots at loss event positions */
ctx.fillStyle = '#e07b00';
lossEvents.forEach(li => {
const xi = xOf(li + (MAX_TICKS - n));
if (xi >= PAD.left && xi <= PAD.left + cw) {
ctx.beginPath(); ctx.arc(xi, PAD.top + ch, 4, 0, 2 * Math.PI); ctx.fill();
}
});
/* Draw axes and RTT label */
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + ch); ctx.lineTo(PAD.left + cw, PAD.top + ch); ctx.stroke();
ctx.fillStyle = '#555'; ctx.font = '9px system-ui';
ctx.fillText('RTT (time)', PAD.left + cw / 2 - 14, H - 2);
}
/* Update the stats bar below the chart */
function updateStats() {
document.getElementById('statCwnd').textContent = i18nFmt('cwnd: %v%', { v: cwnd });
const avg = totalAcks > 0 ? (totalBytes / totalAcks).toFixed(1) : '—';
document.getElementById('statThroughput').textContent = i18nFmt('avg cwnd: %v%', { v: avg });
const phaseLabel = phase === 'slow-start' ? 'Phase: Slow Start' : 'Phase: Congestion Avoidance';
document.getElementById('statPhase').textContent = phaseLabel;
}
function startStop() {
if (running) {
clearInterval(timer); timer = null; running = false;
document.getElementById('btnRun').textContent = 'Run';
} else {
running = true;
document.getElementById('btnRun').textContent = 'Pause';
timer = setInterval(tick, Math.max(50, 600 - getSpeed() * 55));
}
}
document.getElementById('btnRun').addEventListener('click', startStop);
document.getElementById('btnStep').addEventListener('click', () => { if (!running) tick(); });
document.getElementById('btnReset').addEventListener('click', resetState);
document.getElementById('lossSlider').addEventListener('input', function () {
document.getElementById('lossVal').textContent = this.value + '%';
});
document.getElementById('speedSlider').addEventListener('input', function () {
document.getElementById('speedVal').textContent = this.value;
if (running) { clearInterval(timer); timer = setInterval(tick, Math.max(50, 600 - getSpeed() * 55)); }
});
resetState();
<!-- TCP Congestion Control demo — AIMD sawtooth simulator -->
<div class="controls">
<label>Loss prob: <input type="range" id="lossSlider" min="1" max="30" value="8"> <span id="lossVal">8%</span></label>
<label>Speed: <input type="range" id="speedSlider" min="1" max="10" value="5"> <span id="speedVal">5</span></label>
</div>
<canvas id="chart" width="560" height="220" aria-label="TCP congestion window over time"></canvas>
<div class="stats">
<span id="statCwnd">cwnd: 1</span>
<span id="statThroughput">avg cwnd: —</span>
<span id="statPhase">Phase: Slow Start</span>
</div>
<div class="btns">
<button id="btnRun" type="button">Run</button>
<button id="btnStep" type="button">Step</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="dot blue"></span>cwnd
<span class="dot red"></span>ssthresh
<span class="dot orange"></span>loss event
</div>
/* Base styles for the TCP congestion control demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; margin-bottom: .5rem; font-size: .85rem; }
.controls label { display: flex; align-items: center; gap: .4rem; }
canvas { display: block; width: 100%; max-width: 560px; border: 1px solid #d0d7de; border-radius: 8px; background: #fafbfc; }
.stats { display: flex; flex-wrap: wrap; gap: .5rem 1.2rem; margin: .4rem 0; font-size: .85rem; font-weight: 600; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.legend { display: flex; align-items: center; gap: .5rem; font-size: .78rem; color: #555; flex-wrap: wrap; margin-top: .3rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 2px; }
.dot.blue { background: #1d76c8; }
.dot.red { background: #c92f3c; }
.dot.orange { background: #e07b00; }
/* AIMD simulation — TCP Reno congestion control */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Simulation state variables */
let cwnd = 1;
let ssthresh = 16;
let phase = 'slow-start'; /* values: slow-start | congestion-avoidance */
let history = [];
let lossEvents = [];
let running = false;
let timer = null;
let totalAcks = 0;
let totalBytes = 0;
const MAX_TICKS = 80;
const MAX_CWND = 64;
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 12, right: 12, bottom: 28, left: 36 };
/* Read loss probability and speed from sliders */
function getLoss() { return parseInt(document.getElementById('lossSlider').value, 10) / 100; }
function getSpeed() { return parseInt(document.getElementById('speedSlider').value, 10); }
/* Reset all simulation state to initial values */
function resetState() {
cwnd = 1; ssthresh = 16; phase = 'slow-start';
history = []; lossEvents = [];
totalAcks = 0; totalBytes = 0;
running = false;
if (timer) { clearInterval(timer); timer = null; }
document.getElementById('btnRun').textContent = 'Run';
draw(); updateStats();
}
/* One RTT tick: decide loss, update window, record history */
function tick() {
const p = getLoss();
const lost = Math.random() < p;
if (lost) {
/* Loss detected: set ssthresh = cwnd/2, reset cwnd to 1 */
ssthresh = Math.max(Math.floor(cwnd / 2), 2);
cwnd = 1;
phase = 'slow-start';
lossEvents.push(history.length);
} else {
totalAcks++;
totalBytes += cwnd;
if (phase === 'slow-start') {
cwnd = Math.min(cwnd * 2, MAX_CWND); /* Slow start: double cwnd each RTT */
if (cwnd >= ssthresh) phase = 'congestion-avoidance';
} else {
cwnd = Math.min(cwnd + 1, MAX_CWND); /* Congestion avoidance: increase cwnd by 1 each RTT */
}
}
history.push({ cwnd, ssthresh });
/* Keep history bounded to MAX_TICKS entries */
if (history.length > MAX_TICKS) {
history.shift();
lossEvents = lossEvents.map(i => i - 1).filter(i => i >= 0);
}
draw(); updateStats();
}
/* Redraw the entire canvas chart */
function draw() {
ctx.clearRect(0, 0, W, H);
const cw = W - PAD.left - PAD.right;
const ch = H - PAD.top - PAD.bottom;
/* Draw horizontal grid lines at multiples of 8 */
ctx.strokeStyle = '#e0e4e8'; ctx.lineWidth = 0.5;
for (let g = 0; g <= MAX_CWND; g += 8) {
const y = PAD.top + ch - (g / MAX_CWND) * ch;
ctx.beginPath(); ctx.moveTo(PAD.left, y); ctx.lineTo(PAD.left + cw, y); ctx.stroke();
ctx.fillStyle = '#888'; ctx.font = '9px system-ui';
ctx.fillText(g, 2, y + 3);
}
if (history.length < 2) return;
const n = history.length;
const xOf = i => PAD.left + (i / (MAX_TICKS - 1)) * cw;
const yOf = v => PAD.top + ch - (Math.min(v, MAX_CWND) / MAX_CWND) * ch;
/* Draw ssthresh as a dashed red line */
ctx.strokeStyle = '#c92f3c'; ctx.lineWidth = 1; ctx.setLineDash([4, 3]);
ctx.beginPath();
history.forEach((h, i) => {
const x = xOf(i + (MAX_TICKS - n)), y = yOf(h.ssthresh);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke(); ctx.setLineDash([]);
/* Draw congestion window as a solid blue line */
ctx.strokeStyle = '#1d76c8'; ctx.lineWidth = 2;
ctx.beginPath();
history.forEach((h, i) => {
const x = xOf(i + (MAX_TICKS - n)), y = yOf(h.cwnd);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
/* Draw orange dots at loss event positions */
ctx.fillStyle = '#e07b00';
lossEvents.forEach(li => {
const xi = xOf(li + (MAX_TICKS - n));
if (xi >= PAD.left && xi <= PAD.left + cw) {
ctx.beginPath(); ctx.arc(xi, PAD.top + ch, 4, 0, 2 * Math.PI); ctx.fill();
}
});
/* Draw axes and RTT label */
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + ch); ctx.lineTo(PAD.left + cw, PAD.top + ch); ctx.stroke();
ctx.fillStyle = '#555'; ctx.font = '9px system-ui';
ctx.fillText('RTT (time)', PAD.left + cw / 2 - 14, H - 2);
}
/* Update the stats bar below the chart */
function updateStats() {
document.getElementById('statCwnd').textContent = i18nFmt('cwnd: %v%', { v: cwnd });
const avg = totalAcks > 0 ? (totalBytes / totalAcks).toFixed(1) : '—';
document.getElementById('statThroughput').textContent = i18nFmt('avg cwnd: %v%', { v: avg });
const phaseLabel = phase === 'slow-start' ? 'Phase: Slow Start' : 'Phase: Congestion Avoidance';
document.getElementById('statPhase').textContent = phaseLabel;
}
function startStop() {
if (running) {
clearInterval(timer); timer = null; running = false;
document.getElementById('btnRun').textContent = 'Run';
} else {
running = true;
document.getElementById('btnRun').textContent = 'Pause';
timer = setInterval(tick, Math.max(50, 600 - getSpeed() * 55));
}
}
document.getElementById('btnRun').addEventListener('click', startStop);
document.getElementById('btnStep').addEventListener('click', () => { if (!running) tick(); });
document.getElementById('btnReset').addEventListener('click', resetState);
document.getElementById('lossSlider').addEventListener('input', function () {
document.getElementById('lossVal').textContent = this.value + '%';
});
document.getElementById('speedSlider').addEventListener('input', function () {
document.getElementById('speedVal').textContent = this.value;
if (running) { clearInterval(timer); timer = setInterval(tick, Math.max(50, 600 - getSpeed() * 55)); }
});
resetState();
Notice that the window never stays constant: it probes upward, detects a loss signal, retreats, and probes again. Adjust the loss probability slider to make drops rarer or more frequent and watch how the average throughput changes. The sawtooth is not a bug — it is the algorithm doing exactly what it was designed to do.
Comments
Loading comments...