<p class="hint">Each <b>Write</b> appends a key→value pair to the in-memory MemTable.
When it fills up it <b>flushes</b> to a new immutable SSTable on disk. Hit <b>Compact</b>
to merge all SSTables — only the latest value for each key survives.</p>
<div class="lsm-wrap">
<div class="panel" id="panel-mem">
<div class="panel-title">MemTable <span class="badge" id="mem-badge">0/4</span></div>
<div class="entries" id="mem-entries"></div>
</div>
<div class="panel" id="panel-disk">
<div class="panel-title">SSTables on Disk</div>
<div id="sstables"></div>
</div>
</div>
<div class="status" id="status">Write some keys to get started.</div>
<div class="btns">
<button id="btn-write" type="button">Write key</button>
<button id="btn-compact" type="button" disabled>Compact</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.lsm-wrap { display: flex; gap: 10px; margin-bottom: .6rem; }
.panel { flex: 1; border: 1.5px solid #cdd9e3; border-radius: 10px; padding: 8px 10px; min-height: 120px; background: #f5f8fb; }
#panel-disk { flex: 2; }
.panel-title { font-weight: 700; font-size: .82rem; color: #1d3557; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
.badge { background: #1d3557; color: #fff; border-radius: 20px; font-size: .7rem; padding: 1px 6px; font-weight: 600; }
.entries { display: flex; flex-direction: column; gap: 3px; }
.entry { display: flex; gap: 4px; align-items: center; }
.key { background: #1d3557; color: #fff; border-radius: 5px; padding: 2px 7px; font: 600 12px ui-monospace, monospace; }
.val { font: 400 12px ui-monospace, monospace; color: #444; }
.arrow { color: #888; font-size: 11px; }
#sstables { display: flex; flex-wrap: wrap; gap: 6px; }
.sst { border: 1.5px solid #adb1b8; border-radius: 8px; padding: 5px 8px; background: #fff; min-width: 80px; }
.sst-title { font-size: .72rem; font-weight: 700; color: #888; margin-bottom: 4px; }
.sst.compacted { border-color: #0a7d33; background: #f0fff4; }
.sst.compacted .sst-title { color: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; color: #333; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
@keyframes pop { 0%{transform:scale(1.18)} 100%{transform:scale(1)} }
.pop { animation: pop .2s ease-out; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// LSM-tree demo: MemTable → flush → SSTable → compact
// Keys cycle through a small alphabet so duplicates appear naturally.
const KEYS = ['A','B','C','D','E','F','G','H'];
const MEM_CAPACITY = 4; // entries before flush
let memTable = []; // [{key,val,ts}] most-recent first
let ssTables = []; // [{id, entries:[{key,val,ts}]}] newest first
let ts = 0; // logical timestamp
let sstId = 0;
function nextKey() {
// weighted random: keys A-D appear more often to create duplicates
const pool = ['A','A','B','B','C','C','D','E','F','G','H'];
return pool[Math.floor(Math.random() * pool.length)];
}
function nextVal() { return 'v' + (++ts); }
function write() {
const key = nextKey(), val = nextVal();
// Insert or update in memTable (keep all — flush will deduplicate via timestamp)
memTable.unshift({key, val, ts});
setStatus(fmt('Wrote %key% → %val% (ts=%ts%)', { key: key, val: val, ts: ts }), 'info');
if (memTable.length >= MEM_CAPACITY) flush();
render();
}
function flush() {
// Sort by key asc, then ts desc (newest per key first)
const sorted = [...memTable].sort((a,b) => a.key < b.key ? -1 : a.key > b.key ? 1 : b.ts - a.ts);
ssTables.unshift({id: ++sstId, entries: sorted});
memTable = [];
setStatus(fmt('Flushed MemTable → SSTable-%sstId%', { sstId: sstId }), 'info');
document.getElementById('btn-compact').disabled = ssTables.length < 2;
}
function compact() {
// Merge all SSTables: for each key keep only the entry with highest ts
const map = new Map();
// Iterate newest SSTable first so the first occurrence wins (highest ts)
for (const sst of ssTables) {
for (const e of sst.entries) {
if (!map.has(e.key)) map.set(e.key, e);
}
}
const merged = [...map.values()].sort((a,b) => a.key < b.key ? -1 : 1);
ssTables = [{id: ++sstId, entries: merged, compacted: true}];
setStatus(fmt('Compacted %v% SSTables → SSTable-%sstId% (%length% unique keys)', { v: ssTables.length === 1 ? 'all' : '', sstId: sstId, length: merged.length }), 'ok');
document.getElementById('btn-compact').disabled = true;
render();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + (cls || '');
}
function renderEntry(e) {
const div = document.createElement('div'); div.className = 'entry';
div.innerHTML = '<span class="key">' + e.key + '</span>' +
'<span class="arrow">→</span><span class="val">' + e.val + '</span>';
return div;
}
function render() {
// MemTable
const me = document.getElementById('mem-entries'); me.innerHTML = '';
memTable.forEach(e => me.appendChild(renderEntry(e)));
document.getElementById('mem-badge').textContent = memTable.length + '/' + MEM_CAPACITY;
if (memTable.length >= MEM_CAPACITY - 1) {
document.getElementById('mem-badge').style.background = '#e63946';
} else {
document.getElementById('mem-badge').style.background = '#1d3557';
}
// SSTables
const sc = document.getElementById('sstables'); sc.innerHTML = '';
ssTables.forEach(sst => {
const div = document.createElement('div');
div.className = 'sst' + (sst.compacted ? ' compacted' : '');
div.innerHTML = '<div class="sst-title">SSTable-' + sst.id + (sst.compacted ? ' ✓' : '') + '</div>';
const entries = document.createElement('div'); entries.className = 'entries';
sst.entries.forEach(e => entries.appendChild(renderEntry(e)));
div.appendChild(entries); sc.appendChild(div);
});
document.getElementById('btn-compact').disabled = ssTables.length < 2;
}
// Verify: compact merges correctly — most recent ts per key wins
(function selfTest() {
const a = {key:'A', val:'v1', ts:1};
const b = {key:'A', val:'v3', ts:3};
const c = {key:'B', val:'v2', ts:2};
const testSSTs = [{id:1,entries:[a,c]},{id:2,entries:[b]}]; // newest first
const map = new Map();
for (const sst of testSSTs) for (const e of sst.entries) if (!map.has(e.key)) map.set(e.key, e);
const merged = [...map.values()].sort((x,y) => x.key < y.key ? -1 : 1);
console.assert(merged.length === 2, 'compact: 2 unique keys');
console.assert(merged[0].key === 'A' && merged[0].ts === 3, 'compact: A→v3 wins');
console.assert(merged[1].key === 'B' && merged[1].ts === 2, 'compact: B→v2 present');
console.log('LSM demo self-test: PASS');
})();
document.getElementById('btn-write').addEventListener('click', write);
document.getElementById('btn-compact').addEventListener('click', compact);
document.getElementById('btn-reset').addEventListener('click', () => {
memTable = []; ssTables = []; ts = 0; sstId = 0;
document.getElementById('btn-compact').disabled = true;
setStatus('Write some keys to get started.', '');
render();
});
render();
<p class="hint">Each <b>Write</b> appends a key→value pair to the in-memory MemTable.
When it fills up it <b>flushes</b> to a new immutable SSTable on disk. Hit <b>Compact</b>
to merge all SSTables — only the latest value for each key survives.</p>
<div class="lsm-wrap">
<div class="panel" id="panel-mem">
<div class="panel-title">MemTable <span class="badge" id="mem-badge">0/4</span></div>
<div class="entries" id="mem-entries"></div>
</div>
<div class="panel" id="panel-disk">
<div class="panel-title">SSTables on Disk</div>
<div id="sstables"></div>
</div>
</div>
<div class="status" id="status">Write some keys to get started.</div>
<div class="btns">
<button id="btn-write" type="button">Write key</button>
<button id="btn-compact" type="button" disabled>Compact</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.lsm-wrap { display: flex; gap: 10px; margin-bottom: .6rem; }
.panel { flex: 1; border: 1.5px solid #cdd9e3; border-radius: 10px; padding: 8px 10px; min-height: 120px; background: #f5f8fb; }
#panel-disk { flex: 2; }
.panel-title { font-weight: 700; font-size: .82rem; color: #1d3557; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
.badge { background: #1d3557; color: #fff; border-radius: 20px; font-size: .7rem; padding: 1px 6px; font-weight: 600; }
.entries { display: flex; flex-direction: column; gap: 3px; }
.entry { display: flex; gap: 4px; align-items: center; }
.key { background: #1d3557; color: #fff; border-radius: 5px; padding: 2px 7px; font: 600 12px ui-monospace, monospace; }
.val { font: 400 12px ui-monospace, monospace; color: #444; }
.arrow { color: #888; font-size: 11px; }
#sstables { display: flex; flex-wrap: wrap; gap: 6px; }
.sst { border: 1.5px solid #adb1b8; border-radius: 8px; padding: 5px 8px; background: #fff; min-width: 80px; }
.sst-title { font-size: .72rem; font-weight: 700; color: #888; margin-bottom: 4px; }
.sst.compacted { border-color: #0a7d33; background: #f0fff4; }
.sst.compacted .sst-title { color: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; color: #333; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
@keyframes pop { 0%{transform:scale(1.18)} 100%{transform:scale(1)} }
.pop { animation: pop .2s ease-out; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// LSM-tree demo: MemTable → flush → SSTable → compact
// Keys cycle through a small alphabet so duplicates appear naturally.
const KEYS = ['A','B','C','D','E','F','G','H'];
const MEM_CAPACITY = 4; // entries before flush
let memTable = []; // [{key,val,ts}] most-recent first
let ssTables = []; // [{id, entries:[{key,val,ts}]}] newest first
let ts = 0; // logical timestamp
let sstId = 0;
function nextKey() {
// weighted random: keys A-D appear more often to create duplicates
const pool = ['A','A','B','B','C','C','D','E','F','G','H'];
return pool[Math.floor(Math.random() * pool.length)];
}
function nextVal() { return 'v' + (++ts); }
function write() {
const key = nextKey(), val = nextVal();
// Insert or update in memTable (keep all — flush will deduplicate via timestamp)
memTable.unshift({key, val, ts});
setStatus(fmt('Wrote %key% → %val% (ts=%ts%)', { key: key, val: val, ts: ts }), 'info');
if (memTable.length >= MEM_CAPACITY) flush();
render();
}
function flush() {
// Sort by key asc, then ts desc (newest per key first)
const sorted = [...memTable].sort((a,b) => a.key < b.key ? -1 : a.key > b.key ? 1 : b.ts - a.ts);
ssTables.unshift({id: ++sstId, entries: sorted});
memTable = [];
setStatus(fmt('Flushed MemTable → SSTable-%sstId%', { sstId: sstId }), 'info');
document.getElementById('btn-compact').disabled = ssTables.length < 2;
}
function compact() {
// Merge all SSTables: for each key keep only the entry with highest ts
const map = new Map();
// Iterate newest SSTable first so the first occurrence wins (highest ts)
for (const sst of ssTables) {
for (const e of sst.entries) {
if (!map.has(e.key)) map.set(e.key, e);
}
}
const merged = [...map.values()].sort((a,b) => a.key < b.key ? -1 : 1);
ssTables = [{id: ++sstId, entries: merged, compacted: true}];
setStatus(fmt('Compacted %v% SSTables → SSTable-%sstId% (%length% unique keys)', { v: ssTables.length === 1 ? 'all' : '', sstId: sstId, length: merged.length }), 'ok');
document.getElementById('btn-compact').disabled = true;
render();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + (cls || '');
}
function renderEntry(e) {
const div = document.createElement('div'); div.className = 'entry';
div.innerHTML = '<span class="key">' + e.key + '</span>' +
'<span class="arrow">→</span><span class="val">' + e.val + '</span>';
return div;
}
function render() {
// MemTable
const me = document.getElementById('mem-entries'); me.innerHTML = '';
memTable.forEach(e => me.appendChild(renderEntry(e)));
document.getElementById('mem-badge').textContent = memTable.length + '/' + MEM_CAPACITY;
if (memTable.length >= MEM_CAPACITY - 1) {
document.getElementById('mem-badge').style.background = '#e63946';
} else {
document.getElementById('mem-badge').style.background = '#1d3557';
}
// SSTables
const sc = document.getElementById('sstables'); sc.innerHTML = '';
ssTables.forEach(sst => {
const div = document.createElement('div');
div.className = 'sst' + (sst.compacted ? ' compacted' : '');
div.innerHTML = '<div class="sst-title">SSTable-' + sst.id + (sst.compacted ? ' ✓' : '') + '</div>';
const entries = document.createElement('div'); entries.className = 'entries';
sst.entries.forEach(e => entries.appendChild(renderEntry(e)));
div.appendChild(entries); sc.appendChild(div);
});
document.getElementById('btn-compact').disabled = ssTables.length < 2;
}
// Verify: compact merges correctly — most recent ts per key wins
(function selfTest() {
const a = {key:'A', val:'v1', ts:1};
const b = {key:'A', val:'v3', ts:3};
const c = {key:'B', val:'v2', ts:2};
const testSSTs = [{id:1,entries:[a,c]},{id:2,entries:[b]}]; // newest first
const map = new Map();
for (const sst of testSSTs) for (const e of sst.entries) if (!map.has(e.key)) map.set(e.key, e);
const merged = [...map.values()].sort((x,y) => x.key < y.key ? -1 : 1);
console.assert(merged.length === 2, 'compact: 2 unique keys');
console.assert(merged[0].key === 'A' && merged[0].ts === 3, 'compact: A→v3 wins');
console.assert(merged[1].key === 'B' && merged[1].ts === 2, 'compact: B→v2 present');
console.log('LSM demo self-test: PASS');
})();
document.getElementById('btn-write').addEventListener('click', write);
document.getElementById('btn-compact').addEventListener('click', compact);
document.getElementById('btn-reset').addEventListener('click', () => {
memTable = []; ssTables = []; ts = 0; sstId = 0;
document.getElementById('btn-compact').disabled = true;
setStatus('Write some keys to get started.', '');
render();
});
render();
Comments
Loading comments...