The simulation below shows a producer generating messages and a consumer processing them. The buffer in the middle has a fixed capacity of 10 slots.
<div class="panel">
<div class="controls">
<label>Producer speed
<input type="range" id="prodSpeed" min="1" max="10" value="5">
<span id="prodVal">5</span>/s
</label>
<label>Consumer speed
<input type="range" id="consSpeed" min="1" max="10" value="3">
<span id="consVal">3</span>/s
</label>
</div>
<div class="pipeline">
<div class="actor" id="producer">
<div class="actor-label">Producer</div>
<div class="actor-state" id="prodState">IDLE</div>
</div>
<div class="arrow" id="arrowIn">→</div>
<div class="buffer-wrap">
<div class="buffer-label">Buffer <span id="bufCount">0</span>/10</div>
<div class="buffer" id="buffer"></div>
</div>
<div class="arrow" id="arrowOut">→</div>
<div class="actor" id="consumer">
<div class="actor-label">Consumer</div>
<div class="actor-state" id="consState">IDLE</div>
</div>
</div>
<div class="stats">
<span>Sent: <b id="sent">0</b></span>
<span>Processed: <b id="processed">0</b></span>
<span>Dropped: <b id="dropped">0</b> (no backpressure mode)</span>
</div>
<div class="btns">
<button id="btnStart">Start</button>
<button id="btnStop" disabled>Stop</button>
<button id="btnReset" class="ghost">Reset</button>
<label class="toggle">
<input type="checkbox" id="chkBackpressure" checked>
Backpressure ON
</label>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.panel { padding: .8rem; max-width: 500px; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .8rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { flex: 1; }
.pipeline { display: flex; align-items: center; gap: .5rem; margin: .6rem 0; }
.actor { border: 2px solid #1d3557; border-radius: 10px; padding: .5rem .8rem;
text-align: center; min-width: 76px; background: #eef2f7; transition: background .2s; }
.actor.running { background: #d4edda; border-color: #0a7d33; }
.actor.paused { background: #fff3cd; border-color: #c08500; }
.actor-label { font-size: .75rem; font-weight: 700; color: #555; }
.actor-state { font-size: .8rem; font-weight: 600; margin-top: .15rem; }
.arrow { font-size: 1.4rem; color: #1d3557; flex-shrink: 0; transition: color .2s; }
.arrow.blocked { color: #c92f3c; }
.buffer-wrap { flex: 1; text-align: center; }
.buffer-label { font-size: .78rem; font-weight: 700; color: #555; margin-bottom: .25rem; }
.buffer { display: flex; gap: 3px; flex-wrap: nowrap; justify-content: flex-start;
background: #e8eef3; border-radius: 8px; padding: 5px; min-height: 38px;
border: 1px solid #cdd9e3; overflow: hidden; }
.slot { width: 24px; height: 28px; border-radius: 5px; background: #1d3557;
animation: pop .15s ease-out; }
@keyframes pop { from { transform: scale(.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.slot.empty { background: transparent; border: 1px dashed #b0bec8; }
.stats { font-size: .82rem; display: flex; gap: 1rem; flex-wrap: wrap; margin: .4rem 0; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-top: .5rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
.toggle { font-size: .85rem; display: flex; align-items: center; gap: .3rem; cursor: pointer; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Backpressure demo: bounded buffer of capacity 10.
// Producer fires at `prodRate` items/sec; consumer drains at `consRate` items/sec.
// When backpressure is ON: producer pauses when buffer is full, resumes when space frees.
// When backpressure is OFF: producer always runs; messages are dropped on overflow.
const CAPACITY = 10;
let buffer = [];
let sentCount = 0, processedCount = 0, droppedCount = 0;
let running = false;
let prodTimer = null, consTimer = null;
const bufEl = document.getElementById('buffer');
const bufCount = document.getElementById('bufCount');
const prodStateEl = document.getElementById('prodState');
const consStateEl = document.getElementById('consState');
const prodEl = document.getElementById('producer');
const consEl = document.getElementById('consumer');
const arrowIn = document.getElementById('arrowIn');
const sentEl = document.getElementById('sent');
const processedEl = document.getElementById('processed');
const droppedEl = document.getElementById('dropped');
const prodSpeedEl = document.getElementById('prodSpeed');
const consSpeedEl = document.getElementById('consSpeed');
const prodValEl = document.getElementById('prodVal');
const consValEl = document.getElementById('consVal');
const chkBp = document.getElementById('chkBackpressure');
const btnStart = document.getElementById('btnStart');
const btnStop = document.getElementById('btnStop');
const btnReset = document.getElementById('btnReset');
prodSpeedEl.addEventListener('input', () => { prodValEl.textContent = prodSpeedEl.value; reschedule(); });
consSpeedEl.addEventListener('input', () => { consValEl.textContent = consSpeedEl.value; reschedule(); });
function prodInterval() { return 1000 / parseInt(prodSpeedEl.value); }
function consInterval() { return 1000 / parseInt(consSpeedEl.value); }
function renderBuffer() {
bufEl.innerHTML = '';
for (let i = 0; i < CAPACITY; i++) {
const d = document.createElement('div');
d.className = 'slot' + (i < buffer.length ? '' : ' empty');
bufEl.appendChild(d);
}
bufCount.textContent = buffer.length;
}
function setProducerState(state) {
// state: 'idle' | 'running' | 'paused'
prodStateEl.textContent = state.toUpperCase();
prodEl.className = 'actor ' + (state === 'running' ? 'running' : state === 'paused' ? 'paused' : '');
arrowIn.className = 'arrow' + (state === 'paused' ? ' blocked' : '');
}
function setConsumerState(state) {
consStateEl.textContent = state.toUpperCase();
consEl.className = 'actor ' + (state === 'running' ? 'running' : '');
}
function produce() {
if (!running) return;
const bp = chkBp.checked;
if (bp && buffer.length >= CAPACITY) {
// Backpressure: pause producer
setProducerState('paused');
// Re-check after a short poll interval
prodTimer = setTimeout(produce, 100);
return;
}
if (!bp && buffer.length >= CAPACITY) {
// No backpressure: drop the message
droppedCount++;
droppedEl.textContent = droppedCount;
} else {
buffer.push(Date.now());
sentCount++;
sentEl.textContent = sentCount;
renderBuffer();
}
setProducerState('running');
prodTimer = setTimeout(produce, prodInterval());
}
function consume() {
if (!running) return;
if (buffer.length > 0) {
buffer.shift();
processedCount++;
processedEl.textContent = processedCount;
renderBuffer();
setConsumerState('running');
// If producer was paused and there's now space, it will self-resume via its own poll
} else {
setConsumerState('idle');
}
consTimer = setTimeout(consume, consInterval());
}
function reschedule() {
if (!running) return;
clearTimeout(prodTimer);
clearTimeout(consTimer);
prodTimer = setTimeout(produce, prodInterval());
consTimer = setTimeout(consume, consInterval());
}
function start() {
if (running) return;
running = true;
btnStart.disabled = true;
btnStop.disabled = false;
setProducerState('running');
setConsumerState('running');
prodTimer = setTimeout(produce, prodInterval());
consTimer = setTimeout(consume, consInterval());
console.log(fmt('Backpressure demo started. Capacity=%CAPACITY% prodRate=%value%/s consRate=%value2%/s', { CAPACITY: CAPACITY, value: prodSpeedEl.value, value2: consSpeedEl.value }));
}
function stop() {
running = false;
clearTimeout(prodTimer);
clearTimeout(consTimer);
setProducerState('idle');
setConsumerState('idle');
btnStart.disabled = false;
btnStop.disabled = true;
}
function reset() {
stop();
buffer = [];
sentCount = 0; processedCount = 0; droppedCount = 0;
sentEl.textContent = '0';
processedEl.textContent = '0';
droppedEl.textContent = '0';
renderBuffer();
console.log('Reset. buffer=[], sent=0, processed=0, dropped=0');
}
btnStart.addEventListener('click', start);
btnStop.addEventListener('click', stop);
btnReset.addEventListener('click', reset);
// Verify core invariant: buffer never exceeds CAPACITY in backpressure mode
function selfTest() {
const savedRunning = running;
running = false;
buffer = [];
let testSent = 0, testDropped = 0;
// Simulate: produce 15 items into a full buffer with BP on
for (let i = 0; i < 15; i++) {
if (buffer.length < CAPACITY) { buffer.push(i); testSent++; }
// else: would be paused — nothing added
}
console.assert(buffer.length <= CAPACITY,
fmt('FAIL: buffer=%length% > CAPACITY=%CAPACITY%', { length: buffer.length, CAPACITY: CAPACITY }));
console.log(fmt('Self-test PASSED: buffer=%length% <= %CAPACITY%', { length: buffer.length, CAPACITY: CAPACITY }));
buffer = [];
running = savedRunning;
}
selfTest();
renderBuffer();
<div class="panel">
<div class="controls">
<label>Producer speed
<input type="range" id="prodSpeed" min="1" max="10" value="5">
<span id="prodVal">5</span>/s
</label>
<label>Consumer speed
<input type="range" id="consSpeed" min="1" max="10" value="3">
<span id="consVal">3</span>/s
</label>
</div>
<div class="pipeline">
<div class="actor" id="producer">
<div class="actor-label">Producer</div>
<div class="actor-state" id="prodState">IDLE</div>
</div>
<div class="arrow" id="arrowIn">→</div>
<div class="buffer-wrap">
<div class="buffer-label">Buffer <span id="bufCount">0</span>/10</div>
<div class="buffer" id="buffer"></div>
</div>
<div class="arrow" id="arrowOut">→</div>
<div class="actor" id="consumer">
<div class="actor-label">Consumer</div>
<div class="actor-state" id="consState">IDLE</div>
</div>
</div>
<div class="stats">
<span>Sent: <b id="sent">0</b></span>
<span>Processed: <b id="processed">0</b></span>
<span>Dropped: <b id="dropped">0</b> (no backpressure mode)</span>
</div>
<div class="btns">
<button id="btnStart">Start</button>
<button id="btnStop" disabled>Stop</button>
<button id="btnReset" class="ghost">Reset</button>
<label class="toggle">
<input type="checkbox" id="chkBackpressure" checked>
Backpressure ON
</label>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.panel { padding: .8rem; max-width: 500px; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .8rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { flex: 1; }
.pipeline { display: flex; align-items: center; gap: .5rem; margin: .6rem 0; }
.actor { border: 2px solid #1d3557; border-radius: 10px; padding: .5rem .8rem;
text-align: center; min-width: 76px; background: #eef2f7; transition: background .2s; }
.actor.running { background: #d4edda; border-color: #0a7d33; }
.actor.paused { background: #fff3cd; border-color: #c08500; }
.actor-label { font-size: .75rem; font-weight: 700; color: #555; }
.actor-state { font-size: .8rem; font-weight: 600; margin-top: .15rem; }
.arrow { font-size: 1.4rem; color: #1d3557; flex-shrink: 0; transition: color .2s; }
.arrow.blocked { color: #c92f3c; }
.buffer-wrap { flex: 1; text-align: center; }
.buffer-label { font-size: .78rem; font-weight: 700; color: #555; margin-bottom: .25rem; }
.buffer { display: flex; gap: 3px; flex-wrap: nowrap; justify-content: flex-start;
background: #e8eef3; border-radius: 8px; padding: 5px; min-height: 38px;
border: 1px solid #cdd9e3; overflow: hidden; }
.slot { width: 24px; height: 28px; border-radius: 5px; background: #1d3557;
animation: pop .15s ease-out; }
@keyframes pop { from { transform: scale(.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.slot.empty { background: transparent; border: 1px dashed #b0bec8; }
.stats { font-size: .82rem; display: flex; gap: 1rem; flex-wrap: wrap; margin: .4rem 0; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-top: .5rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
.toggle { font-size: .85rem; display: flex; align-items: center; gap: .3rem; cursor: pointer; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Backpressure demo: bounded buffer of capacity 10.
// Producer fires at `prodRate` items/sec; consumer drains at `consRate` items/sec.
// When backpressure is ON: producer pauses when buffer is full, resumes when space frees.
// When backpressure is OFF: producer always runs; messages are dropped on overflow.
const CAPACITY = 10;
let buffer = [];
let sentCount = 0, processedCount = 0, droppedCount = 0;
let running = false;
let prodTimer = null, consTimer = null;
const bufEl = document.getElementById('buffer');
const bufCount = document.getElementById('bufCount');
const prodStateEl = document.getElementById('prodState');
const consStateEl = document.getElementById('consState');
const prodEl = document.getElementById('producer');
const consEl = document.getElementById('consumer');
const arrowIn = document.getElementById('arrowIn');
const sentEl = document.getElementById('sent');
const processedEl = document.getElementById('processed');
const droppedEl = document.getElementById('dropped');
const prodSpeedEl = document.getElementById('prodSpeed');
const consSpeedEl = document.getElementById('consSpeed');
const prodValEl = document.getElementById('prodVal');
const consValEl = document.getElementById('consVal');
const chkBp = document.getElementById('chkBackpressure');
const btnStart = document.getElementById('btnStart');
const btnStop = document.getElementById('btnStop');
const btnReset = document.getElementById('btnReset');
prodSpeedEl.addEventListener('input', () => { prodValEl.textContent = prodSpeedEl.value; reschedule(); });
consSpeedEl.addEventListener('input', () => { consValEl.textContent = consSpeedEl.value; reschedule(); });
function prodInterval() { return 1000 / parseInt(prodSpeedEl.value); }
function consInterval() { return 1000 / parseInt(consSpeedEl.value); }
function renderBuffer() {
bufEl.innerHTML = '';
for (let i = 0; i < CAPACITY; i++) {
const d = document.createElement('div');
d.className = 'slot' + (i < buffer.length ? '' : ' empty');
bufEl.appendChild(d);
}
bufCount.textContent = buffer.length;
}
function setProducerState(state) {
// state: 'idle' | 'running' | 'paused'
prodStateEl.textContent = state.toUpperCase();
prodEl.className = 'actor ' + (state === 'running' ? 'running' : state === 'paused' ? 'paused' : '');
arrowIn.className = 'arrow' + (state === 'paused' ? ' blocked' : '');
}
function setConsumerState(state) {
consStateEl.textContent = state.toUpperCase();
consEl.className = 'actor ' + (state === 'running' ? 'running' : '');
}
function produce() {
if (!running) return;
const bp = chkBp.checked;
if (bp && buffer.length >= CAPACITY) {
// Backpressure: pause producer
setProducerState('paused');
// Re-check after a short poll interval
prodTimer = setTimeout(produce, 100);
return;
}
if (!bp && buffer.length >= CAPACITY) {
// No backpressure: drop the message
droppedCount++;
droppedEl.textContent = droppedCount;
} else {
buffer.push(Date.now());
sentCount++;
sentEl.textContent = sentCount;
renderBuffer();
}
setProducerState('running');
prodTimer = setTimeout(produce, prodInterval());
}
function consume() {
if (!running) return;
if (buffer.length > 0) {
buffer.shift();
processedCount++;
processedEl.textContent = processedCount;
renderBuffer();
setConsumerState('running');
// If producer was paused and there's now space, it will self-resume via its own poll
} else {
setConsumerState('idle');
}
consTimer = setTimeout(consume, consInterval());
}
function reschedule() {
if (!running) return;
clearTimeout(prodTimer);
clearTimeout(consTimer);
prodTimer = setTimeout(produce, prodInterval());
consTimer = setTimeout(consume, consInterval());
}
function start() {
if (running) return;
running = true;
btnStart.disabled = true;
btnStop.disabled = false;
setProducerState('running');
setConsumerState('running');
prodTimer = setTimeout(produce, prodInterval());
consTimer = setTimeout(consume, consInterval());
console.log(fmt('Backpressure demo started. Capacity=%CAPACITY% prodRate=%value%/s consRate=%value2%/s', { CAPACITY: CAPACITY, value: prodSpeedEl.value, value2: consSpeedEl.value }));
}
function stop() {
running = false;
clearTimeout(prodTimer);
clearTimeout(consTimer);
setProducerState('idle');
setConsumerState('idle');
btnStart.disabled = false;
btnStop.disabled = true;
}
function reset() {
stop();
buffer = [];
sentCount = 0; processedCount = 0; droppedCount = 0;
sentEl.textContent = '0';
processedEl.textContent = '0';
droppedEl.textContent = '0';
renderBuffer();
console.log('Reset. buffer=[], sent=0, processed=0, dropped=0');
}
btnStart.addEventListener('click', start);
btnStop.addEventListener('click', stop);
btnReset.addEventListener('click', reset);
// Verify core invariant: buffer never exceeds CAPACITY in backpressure mode
function selfTest() {
const savedRunning = running;
running = false;
buffer = [];
let testSent = 0, testDropped = 0;
// Simulate: produce 15 items into a full buffer with BP on
for (let i = 0; i < 15; i++) {
if (buffer.length < CAPACITY) { buffer.push(i); testSent++; }
// else: would be paused — nothing added
}
console.assert(buffer.length <= CAPACITY,
fmt('FAIL: buffer=%length% > CAPACITY=%CAPACITY%', { length: buffer.length, CAPACITY: CAPACITY }));
console.log(fmt('Self-test PASSED: buffer=%length% <= %CAPACITY%', { length: buffer.length, CAPACITY: CAPACITY }));
buffer = [];
running = savedRunning;
}
selfTest();
renderBuffer();
Click Start and watch the buffer fill. When it reaches capacity, the producer automatically pauses — that pause is backpressure in action. As soon as the consumer drains a slot, the producer resumes. Try cranking up the producer speed or slowing down the consumer to see how the system adapts. Without backpressure, the buffer would overflow and messages would be lost.
Comments
Loading comments...