Below is a simulation of the reactor loop. Adjust the number of connections and watch a single thread dispatch only the ones that become ready each tick — just as epoll_wait works in the real kernel.
<!-- Reactor demo: circles = sockets, orange = ready (epoll notified), green = dispatched this tick -->
<div class="controls">
<label>
Connections:
<input type="range" id="connCount" min="10" max="200" value="60" step="10">
<span id="connVal">60</span>
</label>
<label>
Ready % per tick:
<input type="range" id="readyPct" min="0" max="100" value="20" step="5">
<span id="pctVal">20%</span>
</label>
</div>
<div class="legend">
<span class="dot idle"></span> Idle
<span class="dot ready"></span> Ready (epoll notified)
<span class="dot active"></span> Dispatched this tick
</div>
<canvas id="canvas" width="460" height="220"></canvas>
<div class="stats" id="stats"></div>
<div class="btns">
<button id="btnTick" type="button">One tick</button>
<button id="btnAuto" type="button">Auto-run</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .6rem; font-size: .85rem; }
.controls label { display: flex; align-items: center; gap: .5rem; }
.controls input[type=range] { flex: 1; }
.controls span { min-width: 3.2rem; text-align: right; font-variant-numeric: tabular-nums; }
.legend { font-size: .8rem; display: flex; gap: .8rem; align-items: center; margin-bottom: .4rem; color: #555; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.idle { background: #c9ccd1; }
.dot.ready { background: #f4a226; }
.dot.active { background: #1d8a4b; }
canvas { display: block; border: 1px solid #dde3ea; border-radius: 8px; background: #f8fafc; max-width: 100%; }
.stats { font-size: .88rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #1d3557; }
.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; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simulation state — one thread handles all connections
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const connSlider = document.getElementById('connCount');
const pctSlider = document.getElementById('readyPct');
const connVal = document.getElementById('connVal');
const pctVal = document.getElementById('pctVal');
const statsEl = document.getElementById('stats');
const btnTick = document.getElementById('btnTick');
const btnAuto = document.getElementById('btnAuto');
const btnReset = document.getElementById('btnReset');
let connections = []; // array of all open sockets; state: 0=idle, 1=ready, 2=dispatched
let totalDispatched = 0;
let tick = 0;
let autoTimer = null;
// Connection states
// 0 = idle, 1 = ready, 2 = dispatched-this-tick
function buildConnections() {
const n = parseInt(connSlider.value, 10);
connections = Array.from({ length: n }, (_, i) => ({ id: i, state: 0 }));
totalDispatched = 0;
tick = 0;
}
// markReady — simulates the kernel delivering readiness events via epoll_wait
// Mark a random subset of idle connections as ready (simulates kernel readiness list)
function markReady() {
const pct = parseInt(pctSlider.value, 10) / 100;
connections.forEach(c => {
if (c.state === 2) c.state = 0; // reset dispatched back to idle after one tick
if (c.state === 0 && Math.random() < pct) c.state = 1;
});
}
// dispatchReady — the reactor loop: O(ready), not O(total)
// The reactor: collect all ready connections and dispatch them (O(ready), not O(total))
function dispatchReady() {
let dispatched = 0;
connections.forEach(c => {
if (c.state === 1) { c.state = 2; dispatched++; }
});
totalDispatched += dispatched;
return dispatched;
}
// Render all sockets as colored circles on the canvas
function draw() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const n = connections.length;
const cols = Math.ceil(Math.sqrt(n * (W / H)));
const rows = Math.ceil(n / cols);
const cw = Math.floor(W / cols);
const ch = Math.floor(H / rows);
const r = Math.max(3, Math.min(cw, ch) / 2 - 2);
connections.forEach((c, i) => {
const col = i % cols;
const row = Math.floor(i / cols);
const x = cw * col + cw / 2;
const y = ch * row + ch / 2;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
if (c.state === 2) ctx.fillStyle = '#1d8a4b';
else if (c.state === 1) ctx.fillStyle = '#f4a226';
else ctx.fillStyle = '#c9ccd1';
ctx.fill();
});
}
function updateStats(dispatched) {
const total = connections.length;
const ready = connections.filter(c => c.state === 1).length;
const pct = total > 0 ? Math.round(dispatched / total * 100) : 0;
statsEl.textContent = i18nFmt('Tick %tick%: dispatched %dispatched% / %total% sockets (%pct%% of total)', { tick: tick, dispatched: dispatched, total: total, pct: pct });
}
function doTick() {
tick++;
markReady();
const dispatched = dispatchReady();
draw();
updateStats(dispatched);
}
function reset() {
if (autoTimer) { clearInterval(autoTimer); autoTimer = null; btnAuto.textContent = 'Auto-run'; }
buildConnections();
draw();
statsEl.textContent = 'Press ’One tick’ or ’Auto-run’ to start the event loop.';
}
connSlider.addEventListener('input', () => {
connVal.textContent = connSlider.value;
reset();
});
pctSlider.addEventListener('input', () => {
pctVal.textContent = connSlider.value;
pctVal.textContent = pctSlider.value + '%';
reset();
});
btnTick.addEventListener('click', doTick);
btnAuto.addEventListener('click', () => {
if (autoTimer) {
clearInterval(autoTimer);
autoTimer = null;
btnAuto.textContent = 'Auto-run';
} else {
autoTimer = setInterval(doTick, 400);
btnAuto.textContent = 'Pause';
}
});
btnReset.addEventListener('click', reset);
// Initial render
buildConnections();
draw();
statsEl.textContent = 'Press ’One tick’ or ’Auto-run’ to start the event loop.';
<!-- Reactor demo: circles = sockets, orange = ready (epoll notified), green = dispatched this tick -->
<div class="controls">
<label>
Connections:
<input type="range" id="connCount" min="10" max="200" value="60" step="10">
<span id="connVal">60</span>
</label>
<label>
Ready % per tick:
<input type="range" id="readyPct" min="0" max="100" value="20" step="5">
<span id="pctVal">20%</span>
</label>
</div>
<div class="legend">
<span class="dot idle"></span> Idle
<span class="dot ready"></span> Ready (epoll notified)
<span class="dot active"></span> Dispatched this tick
</div>
<canvas id="canvas" width="460" height="220"></canvas>
<div class="stats" id="stats"></div>
<div class="btns">
<button id="btnTick" type="button">One tick</button>
<button id="btnAuto" type="button">Auto-run</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .6rem; font-size: .85rem; }
.controls label { display: flex; align-items: center; gap: .5rem; }
.controls input[type=range] { flex: 1; }
.controls span { min-width: 3.2rem; text-align: right; font-variant-numeric: tabular-nums; }
.legend { font-size: .8rem; display: flex; gap: .8rem; align-items: center; margin-bottom: .4rem; color: #555; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.idle { background: #c9ccd1; }
.dot.ready { background: #f4a226; }
.dot.active { background: #1d8a4b; }
canvas { display: block; border: 1px solid #dde3ea; border-radius: 8px; background: #f8fafc; max-width: 100%; }
.stats { font-size: .88rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #1d3557; }
.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; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simulation state — one thread handles all connections
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const connSlider = document.getElementById('connCount');
const pctSlider = document.getElementById('readyPct');
const connVal = document.getElementById('connVal');
const pctVal = document.getElementById('pctVal');
const statsEl = document.getElementById('stats');
const btnTick = document.getElementById('btnTick');
const btnAuto = document.getElementById('btnAuto');
const btnReset = document.getElementById('btnReset');
let connections = []; // array of all open sockets; state: 0=idle, 1=ready, 2=dispatched
let totalDispatched = 0;
let tick = 0;
let autoTimer = null;
// Connection states
// 0 = idle, 1 = ready, 2 = dispatched-this-tick
function buildConnections() {
const n = parseInt(connSlider.value, 10);
connections = Array.from({ length: n }, (_, i) => ({ id: i, state: 0 }));
totalDispatched = 0;
tick = 0;
}
// markReady — simulates the kernel delivering readiness events via epoll_wait
// Mark a random subset of idle connections as ready (simulates kernel readiness list)
function markReady() {
const pct = parseInt(pctSlider.value, 10) / 100;
connections.forEach(c => {
if (c.state === 2) c.state = 0; // reset dispatched back to idle after one tick
if (c.state === 0 && Math.random() < pct) c.state = 1;
});
}
// dispatchReady — the reactor loop: O(ready), not O(total)
// The reactor: collect all ready connections and dispatch them (O(ready), not O(total))
function dispatchReady() {
let dispatched = 0;
connections.forEach(c => {
if (c.state === 1) { c.state = 2; dispatched++; }
});
totalDispatched += dispatched;
return dispatched;
}
// Render all sockets as colored circles on the canvas
function draw() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const n = connections.length;
const cols = Math.ceil(Math.sqrt(n * (W / H)));
const rows = Math.ceil(n / cols);
const cw = Math.floor(W / cols);
const ch = Math.floor(H / rows);
const r = Math.max(3, Math.min(cw, ch) / 2 - 2);
connections.forEach((c, i) => {
const col = i % cols;
const row = Math.floor(i / cols);
const x = cw * col + cw / 2;
const y = ch * row + ch / 2;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
if (c.state === 2) ctx.fillStyle = '#1d8a4b';
else if (c.state === 1) ctx.fillStyle = '#f4a226';
else ctx.fillStyle = '#c9ccd1';
ctx.fill();
});
}
function updateStats(dispatched) {
const total = connections.length;
const ready = connections.filter(c => c.state === 1).length;
const pct = total > 0 ? Math.round(dispatched / total * 100) : 0;
statsEl.textContent = i18nFmt('Tick %tick%: dispatched %dispatched% / %total% sockets (%pct%% of total)', { tick: tick, dispatched: dispatched, total: total, pct: pct });
}
function doTick() {
tick++;
markReady();
const dispatched = dispatchReady();
draw();
updateStats(dispatched);
}
function reset() {
if (autoTimer) { clearInterval(autoTimer); autoTimer = null; btnAuto.textContent = 'Auto-run'; }
buildConnections();
draw();
statsEl.textContent = 'Press ’One tick’ or ’Auto-run’ to start the event loop.';
}
connSlider.addEventListener('input', () => {
connVal.textContent = connSlider.value;
reset();
});
pctSlider.addEventListener('input', () => {
pctVal.textContent = connSlider.value;
pctVal.textContent = pctSlider.value + '%';
reset();
});
btnTick.addEventListener('click', doTick);
btnAuto.addEventListener('click', () => {
if (autoTimer) {
clearInterval(autoTimer);
autoTimer = null;
btnAuto.textContent = 'Auto-run';
} else {
autoTimer = setInterval(doTick, 400);
btnAuto.textContent = 'Pause';
}
});
btnReset.addEventListener('click', reset);
// Initial render
buildConnections();
draw();
statsEl.textContent = 'Press ’One tick’ or ’Auto-run’ to start the event loop.';
Notice that the thread processes only the ready connections each tick, not all of them. When connections are sparse or idle, the thread does almost nothing. When they all fire at once, it still finishes in one pass — no extra threads needed. This is why epoll scales to 104 or even 105 sockets while a one-thread-per-connection model collapses.
Comments
Loading comments...