Add block requests by clicking on the disk track below, then choose a scheduling algorithm and press Run. Watch the head sweep across the disk and count how many cylinders it travels.
<!-- Interactive I/O scheduler demo — three classic disk scheduling algorithms. -->
<p class="hint">Click anywhere on the track to add a disk block request. Add several, pick a scheduling algorithm, then press <b>Run</b> to watch the disk head sweep through them.</p>
<div class="controls">
<label>Algorithm:
<select id="algo">
<option value="sstf">SSTF</option>
<option value="scan" selected>SCAN (elevator)</option>
<option value="deadline">Deadline</option>
</select>
</label>
<button id="run" type="button">Run</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<!-- Disk track visualization — dots = pending requests, red square = head position. -->
<div class="track-wrap">
<div id="track" class="track"></div>
<div id="head" class="head" title="Disk head"></div>
</div>
<p class="track-label"><span>0</span><span>200 cylinders</span></p>
<!-- Status and serve-order output. -->
<div class="status" id="status">Click the track to add requests.</div>
<div id="order-wrap" class="order-wrap"></div>
/* Base layout and typography. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .4rem; }
select { font: inherit; padding: .2rem .4rem; border-radius: 6px; border: 1px solid #adb1b8; }
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; }
/* Track, dot, and head styles. */
.track-wrap { position: relative; height: 36px; margin: .4rem 0 0; }
.track { position: absolute; top: 50%; left: 0; right: 0; height: 6px;
background: #c9ccd1; border-radius: 3px; transform: translateY(-50%); cursor: crosshair; }
.req-dot { position: absolute; width: 14px; height: 14px; border-radius: 50%;
background: #1d3557; border: 2px solid #fff; top: 50%; transform: translate(-50%, -50%);
cursor: pointer; transition: background .2s; }
.req-dot.served { background: #0a7d33; }
.req-dot.expired { background: #e63946; }
.head { position: absolute; width: 20px; height: 20px; border-radius: 4px;
background: #e63946; top: 50%; transform: translate(-50%, -50%);
border: 2px solid #fff; z-index: 2; transition: left .35s ease-in-out; }
.track-label { display: flex; justify-content: space-between; font-size: .75rem; color: #888; margin: .1rem 0 .4rem; }
/* Status line and serve-order display. */
.status { font-size: .95rem; font-weight: 600; min-height: 1.5em; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.order-wrap { font-size: .82rem; color: #444; line-height: 1.6; min-height: 1.4em; }
.order-wrap .seq-label { font-weight: 600; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Simulation constants. */
const MAX_CYL = 200;
const DEADLINE_MS = 3000; /* requests older than 3 s are considered expired and get priority */
let requests = []; /* request list, each entry has { cyl, arrivedAt, served, expired } { cyl, arrivedAt } */
let headPos = 50;
let running = false;
const trackEl = document.getElementById('track');
const headEl = document.getElementById('head');
const statusEl = document.getElementById('status');
const orderEl = document.getElementById('order-wrap');
/* Convert between percentage position and cylinder number. */
function pctToCyl(pct) { return Math.round(pct * MAX_CYL); }
function cylToPct(cyl) { return cyl / MAX_CYL; }
function renderHead() {
headEl.style.left = (cylToPct(headPos) * 100) + '%';
}
/* Render all pending requests as colored dots on the track. */
function renderDots() {
trackEl.querySelectorAll('.req-dot').forEach(d => d.remove());
requests.forEach((r, i) => {
const d = document.createElement('div');
d.className = 'req-dot' + (r.served ? ' served' : '') + (r.expired ? ' expired' : '');
d.style.left = (cylToPct(r.cyl) * 100) + '%';
d.title = i18nFmt('Cylinder %cyl% — click to remove', { cyl: r.cyl });
/* Allow removing a request by clicking its dot (only when not running). */
d.addEventListener('click', e => {
if (running) return;
requests.splice(i, 1);
renderDots();
});
trackEl.appendChild(d);
});
}
/* Add a new request at the clicked position on the track. */
trackEl.addEventListener('click', e => {
if (running) return;
const rect = trackEl.getBoundingClientRect();
const pct = (e.clientX - rect.left) / rect.width;
const cyl = pctToCyl(Math.max(0, Math.min(1, pct)));
requests.push({ cyl, arrivedAt: Date.now(), served: false, expired: false });
renderDots();
setStatus('Click the track to add requests.', '');
});
function setStatus(t, cls) {
statusEl.textContent = t;
statusEl.className = 'status ' + cls;
}
/* SSTF: always serve the nearest pending request. */
function sstfOrder(head, reqs) {
const rem = reqs.map((r, i) => ({ ...r, idx: i }));
const order = [];
let pos = head;
while (rem.length) {
rem.sort((a, b) => Math.abs(a.cyl - pos) - Math.abs(b.cyl - pos));
const next = rem.shift();
order.push(next.idx);
pos = next.cyl;
}
return order;
}
/* SCAN (elevator): sweep upward first, then reverse. */
function scanOrder(head, reqs) {
const above = reqs.map((r, i) => ({ ...r, idx: i })).filter(r => r.cyl >= head).sort((a, b) => a.cyl - b.cyl);
const below = reqs.map((r, i) => ({ ...r, idx: i })).filter(r => r.cyl < head).sort((a, b) => b.cyl - a.cyl);
return [...above, ...below].map(r => r.idx);
}
/* Deadline: like SCAN but expired requests jump to the front. */
function deadlineOrder(head, reqs, now) {
const expired = reqs.map((r, i) => ({ ...r, idx: i })).filter(r => (now - r.arrivedAt) >= DEADLINE_MS);
if (expired.length) {
/* Serve expired requests first (in SSTF order among themselves), then the rest via SCAN. */
const expOrder = sstfOrder(head, expired.map(r => ({ cyl: r.cyl })));
const expIdxs = expOrder.map(i => expired[i].idx);
const remaining = reqs.map((r, i) => ({ ...r, idx: i })).filter((_, i) => !expIdxs.includes(i));
const restOrder = scanOrder(head, remaining.map(r => ({ cyl: r.cyl, arrivedAt: r.arrivedAt })));
const restIdxs = restOrder.map(i => remaining[i].idx);
return [...expIdxs, ...restIdxs];
}
return scanOrder(head, reqs);
}
/* Animate the head moving through the computed serve order. */
async function animate(order) {
running = true;
document.getElementById('run').disabled = true;
const algo = document.getElementById('algo').value;
let totalSeek = 0;
let prevPos = headPos;
const seqLabels = [];
const now = Date.now();
for (let step = 0; step < order.length; step++) {
const idx = order[step];
const r = requests[idx];
/* Mark visually if this request was already expired when Run was pressed. */
if (algo === 'deadline' && (now - r.arrivedAt) >= DEADLINE_MS) {
r.expired = true;
}
const dest = r.cyl;
totalSeek += Math.abs(dest - prevPos);
headPos = dest;
renderHead();
r.served = true;
renderDots();
seqLabels.push(dest);
setStatus(i18nFmt('Step %step%/%total%: serving cylinder %cyl% (total seek: %seek%)', { step: step + 1, total: order.length, cyl: dest, seek: totalSeek }), '');
await new Promise(res => setTimeout(res, 380));
prevPos = dest;
}
setStatus(i18nFmt('Done! Total seek distance: %seek% cylinders.', { seek: totalSeek }), 'ok');
orderEl.innerHTML = '<span class="seq-label">Serve order:</span> ' + seqLabels.join(' → ');
running = false;
document.getElementById('run').disabled = false;
}
document.getElementById('run').addEventListener('click', () => {
if (running) return;
if (!requests.length) { setStatus('Add at least one request first.', 'bad'); return; }
requests.forEach(r => { r.served = false; r.expired = false; });
const algo = document.getElementById('algo').value;
let order;
/* Dispatch to the selected algorithm. */
if (algo === 'sstf') order = sstfOrder(headPos, requests);
else if (algo === 'scan') order = scanOrder(headPos, requests);
else order = deadlineOrder(headPos, requests, Date.now());
animate(order);
});
document.getElementById('reset').addEventListener('click', () => {
if (running) return;
requests = [];
headPos = 50;
renderHead();
renderDots();
setStatus('Click the track to add requests.', '');
orderEl.innerHTML = '';
});
/* Initial render. */
renderHead();
setStatus('Click the track to add requests.', '');
<!-- Interactive I/O scheduler demo — three classic disk scheduling algorithms. -->
<p class="hint">Click anywhere on the track to add a disk block request. Add several, pick a scheduling algorithm, then press <b>Run</b> to watch the disk head sweep through them.</p>
<div class="controls">
<label>Algorithm:
<select id="algo">
<option value="sstf">SSTF</option>
<option value="scan" selected>SCAN (elevator)</option>
<option value="deadline">Deadline</option>
</select>
</label>
<button id="run" type="button">Run</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<!-- Disk track visualization — dots = pending requests, red square = head position. -->
<div class="track-wrap">
<div id="track" class="track"></div>
<div id="head" class="head" title="Disk head"></div>
</div>
<p class="track-label"><span>0</span><span>200 cylinders</span></p>
<!-- Status and serve-order output. -->
<div class="status" id="status">Click the track to add requests.</div>
<div id="order-wrap" class="order-wrap"></div>
/* Base layout and typography. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .4rem; }
select { font: inherit; padding: .2rem .4rem; border-radius: 6px; border: 1px solid #adb1b8; }
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; }
/* Track, dot, and head styles. */
.track-wrap { position: relative; height: 36px; margin: .4rem 0 0; }
.track { position: absolute; top: 50%; left: 0; right: 0; height: 6px;
background: #c9ccd1; border-radius: 3px; transform: translateY(-50%); cursor: crosshair; }
.req-dot { position: absolute; width: 14px; height: 14px; border-radius: 50%;
background: #1d3557; border: 2px solid #fff; top: 50%; transform: translate(-50%, -50%);
cursor: pointer; transition: background .2s; }
.req-dot.served { background: #0a7d33; }
.req-dot.expired { background: #e63946; }
.head { position: absolute; width: 20px; height: 20px; border-radius: 4px;
background: #e63946; top: 50%; transform: translate(-50%, -50%);
border: 2px solid #fff; z-index: 2; transition: left .35s ease-in-out; }
.track-label { display: flex; justify-content: space-between; font-size: .75rem; color: #888; margin: .1rem 0 .4rem; }
/* Status line and serve-order display. */
.status { font-size: .95rem; font-weight: 600; min-height: 1.5em; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.order-wrap { font-size: .82rem; color: #444; line-height: 1.6; min-height: 1.4em; }
.order-wrap .seq-label { font-weight: 600; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Simulation constants. */
const MAX_CYL = 200;
const DEADLINE_MS = 3000; /* requests older than 3 s are considered expired and get priority */
let requests = []; /* request list, each entry has { cyl, arrivedAt, served, expired } { cyl, arrivedAt } */
let headPos = 50;
let running = false;
const trackEl = document.getElementById('track');
const headEl = document.getElementById('head');
const statusEl = document.getElementById('status');
const orderEl = document.getElementById('order-wrap');
/* Convert between percentage position and cylinder number. */
function pctToCyl(pct) { return Math.round(pct * MAX_CYL); }
function cylToPct(cyl) { return cyl / MAX_CYL; }
function renderHead() {
headEl.style.left = (cylToPct(headPos) * 100) + '%';
}
/* Render all pending requests as colored dots on the track. */
function renderDots() {
trackEl.querySelectorAll('.req-dot').forEach(d => d.remove());
requests.forEach((r, i) => {
const d = document.createElement('div');
d.className = 'req-dot' + (r.served ? ' served' : '') + (r.expired ? ' expired' : '');
d.style.left = (cylToPct(r.cyl) * 100) + '%';
d.title = i18nFmt('Cylinder %cyl% — click to remove', { cyl: r.cyl });
/* Allow removing a request by clicking its dot (only when not running). */
d.addEventListener('click', e => {
if (running) return;
requests.splice(i, 1);
renderDots();
});
trackEl.appendChild(d);
});
}
/* Add a new request at the clicked position on the track. */
trackEl.addEventListener('click', e => {
if (running) return;
const rect = trackEl.getBoundingClientRect();
const pct = (e.clientX - rect.left) / rect.width;
const cyl = pctToCyl(Math.max(0, Math.min(1, pct)));
requests.push({ cyl, arrivedAt: Date.now(), served: false, expired: false });
renderDots();
setStatus('Click the track to add requests.', '');
});
function setStatus(t, cls) {
statusEl.textContent = t;
statusEl.className = 'status ' + cls;
}
/* SSTF: always serve the nearest pending request. */
function sstfOrder(head, reqs) {
const rem = reqs.map((r, i) => ({ ...r, idx: i }));
const order = [];
let pos = head;
while (rem.length) {
rem.sort((a, b) => Math.abs(a.cyl - pos) - Math.abs(b.cyl - pos));
const next = rem.shift();
order.push(next.idx);
pos = next.cyl;
}
return order;
}
/* SCAN (elevator): sweep upward first, then reverse. */
function scanOrder(head, reqs) {
const above = reqs.map((r, i) => ({ ...r, idx: i })).filter(r => r.cyl >= head).sort((a, b) => a.cyl - b.cyl);
const below = reqs.map((r, i) => ({ ...r, idx: i })).filter(r => r.cyl < head).sort((a, b) => b.cyl - a.cyl);
return [...above, ...below].map(r => r.idx);
}
/* Deadline: like SCAN but expired requests jump to the front. */
function deadlineOrder(head, reqs, now) {
const expired = reqs.map((r, i) => ({ ...r, idx: i })).filter(r => (now - r.arrivedAt) >= DEADLINE_MS);
if (expired.length) {
/* Serve expired requests first (in SSTF order among themselves), then the rest via SCAN. */
const expOrder = sstfOrder(head, expired.map(r => ({ cyl: r.cyl })));
const expIdxs = expOrder.map(i => expired[i].idx);
const remaining = reqs.map((r, i) => ({ ...r, idx: i })).filter((_, i) => !expIdxs.includes(i));
const restOrder = scanOrder(head, remaining.map(r => ({ cyl: r.cyl, arrivedAt: r.arrivedAt })));
const restIdxs = restOrder.map(i => remaining[i].idx);
return [...expIdxs, ...restIdxs];
}
return scanOrder(head, reqs);
}
/* Animate the head moving through the computed serve order. */
async function animate(order) {
running = true;
document.getElementById('run').disabled = true;
const algo = document.getElementById('algo').value;
let totalSeek = 0;
let prevPos = headPos;
const seqLabels = [];
const now = Date.now();
for (let step = 0; step < order.length; step++) {
const idx = order[step];
const r = requests[idx];
/* Mark visually if this request was already expired when Run was pressed. */
if (algo === 'deadline' && (now - r.arrivedAt) >= DEADLINE_MS) {
r.expired = true;
}
const dest = r.cyl;
totalSeek += Math.abs(dest - prevPos);
headPos = dest;
renderHead();
r.served = true;
renderDots();
seqLabels.push(dest);
setStatus(i18nFmt('Step %step%/%total%: serving cylinder %cyl% (total seek: %seek%)', { step: step + 1, total: order.length, cyl: dest, seek: totalSeek }), '');
await new Promise(res => setTimeout(res, 380));
prevPos = dest;
}
setStatus(i18nFmt('Done! Total seek distance: %seek% cylinders.', { seek: totalSeek }), 'ok');
orderEl.innerHTML = '<span class="seq-label">Serve order:</span> ' + seqLabels.join(' → ');
running = false;
document.getElementById('run').disabled = false;
}
document.getElementById('run').addEventListener('click', () => {
if (running) return;
if (!requests.length) { setStatus('Add at least one request first.', 'bad'); return; }
requests.forEach(r => { r.served = false; r.expired = false; });
const algo = document.getElementById('algo').value;
let order;
/* Dispatch to the selected algorithm. */
if (algo === 'sstf') order = sstfOrder(headPos, requests);
else if (algo === 'scan') order = scanOrder(headPos, requests);
else order = deadlineOrder(headPos, requests, Date.now());
animate(order);
});
document.getElementById('reset').addEventListener('click', () => {
if (running) return;
requests = [];
headPos = 50;
renderHead();
renderDots();
setStatus('Click the track to add requests.', '');
orderEl.innerHTML = '';
});
/* Initial render. */
renderHead();
setStatus('Click the track to add requests.', '');
Comments
Loading comments...