Below you can insert items into three filter variants side by side, then delete from the ones that support it, and finally query to see which returns a false positive.
<!-- Bloom filter variants comparison demo -->
<div class="demo-wrap">
<div class="controls">
<label for="item-input">Item to insert / delete / query:</label>
<div class="input-row">
<input id="item-input" type="text" placeholder="e.g. alice" maxlength="20" />
<button id="btn-insert" type="button">Insert</button>
</div>
<div id="msg" class="msg"></div>
</div>
<div class="filters-grid">
<div class="filter-card" id="card-classic">
<div class="filter-name">Classic Bloom</div>
<div class="filter-desc">k=3 hash functions, m=32 bits. No delete.</div>
<canvas class="bits-canvas" id="bits-classic" width="320" height="36" title="Bit array (dark = 1)"></canvas>
<div class="filter-actions">
<button class="btn-del disabled" id="del-classic" type="button" disabled title="This filter does not support deletion">Delete</button>
<button class="btn-query" id="query-classic" type="button">Query</button>
</div>
<div class="result" id="result-classic"></div>
<div class="stat" id="stat-classic"></div>
</div>
<div class="filter-card" id="card-counting">
<div class="filter-name">Counting Filter</div>
<div class="filter-desc">k=3 counters (4-bit each), m=32 slots. Delete supported.</div>
<canvas class="bits-canvas" id="bits-counting" width="320" height="36" title="Counter array (dark = non-zero, number = count)"></canvas>
<div class="filter-actions">
<button class="btn-del" id="del-counting" type="button">Delete</button>
<button class="btn-query" id="query-counting" type="button">Query</button>
</div>
<div class="result" id="result-counting"></div>
<div class="stat" id="stat-counting"></div>
</div>
<div class="filter-card" id="card-cuckoo">
<div class="filter-name">Cuckoo Filter</div>
<div class="filter-desc">8-bit fingerprints, 16 buckets × 4 slots. Delete supported.</div>
<div class="cuckoo-buckets" id="buckets-cuckoo" title="Fingerprint slots (hex, red = queried fingerprint)"></div>
<div class="filter-actions">
<button class="btn-del" id="del-cuckoo" type="button">Delete</button>
<button class="btn-query" id="query-cuckoo" type="button">Query</button>
</div>
<div class="result" id="result-cuckoo"></div>
<div class="stat" id="stat-cuckoo"></div>
</div>
<div class="filter-card" id="card-blocked">
<div class="filter-name">Blocked Bloom</div>
<div class="filter-desc">k=3 per block, 2 blocks of 16 bits. No delete, 1 cache line.</div>
<canvas class="bits-canvas" id="bits-blocked" width="320" height="36" title="Blocked bit array (two cache-line blocks)"></canvas>
<div class="filter-actions">
<button class="btn-del disabled" id="del-blocked" type="button" disabled title="This filter does not support deletion">Delete</button>
<button class="btn-query" id="query-blocked" type="button">Query</button>
</div>
<div class="result" id="result-blocked"></div>
<div class="stat" id="stat-blocked"></div>
</div>
</div>
<div class="legend">
<button id="btn-reset" type="button" class="ghost">Reset all</button>
<span class="legend-note">Red = queried positions · Orange = probably present · Green = absent · Crimson = false positive</span>
</div>
</div>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; font-size: 14px; }
.demo-wrap { padding: .6rem .5rem; }
.controls { margin-bottom: .8rem; }
.controls label { font-weight: 600; display: block; margin-bottom: .3rem; }
.input-row { display: flex; gap: .4rem; margin-bottom: .35rem; }
#item-input { flex: 1; padding: .4rem .6rem; border: 1px solid #bbb; border-radius: 6px; font-size: 14px; }
.msg { font-size: .85rem; color: #666; min-height: 1.2em; }
.msg.err { color: #c92f3c; }
.filters-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
@media (max-width: 500px) { .filters-grid { grid-template-columns: 1fr; } }
.filter-card { border: 1px solid #dde3ea; border-radius: 10px; padding: .6rem .7rem; background: #f9fafb; }
.filter-name { font-weight: 700; font-size: .92rem; margin-bottom: .15rem; }
.filter-desc { font-size: .76rem; color: #555; margin-bottom: .4rem; line-height: 1.4; }
.bits-canvas { display: block; width: 100%; height: 36px; border-radius: 4px; margin-bottom: .4rem; background: #e8eef3; }
.cuckoo-buckets { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: .4rem; min-height: 36px; align-content: flex-start; }
.bucket { width: 26px; height: 26px; border-radius: 4px; display: flex; align-items: center; justify-content: center; font: 600 9px ui-monospace, monospace; background: #e8eef3; border: 1px solid #cdd9e3; color: #1d3557; }
.bucket.used { background: #1d3557; color: #fff; border-color: #1d3557; }
.filter-actions { display: flex; gap: .35rem; margin-bottom: .35rem; }
button { font: 600 13px system-ui; padding: .35rem .7rem; border-radius: 6px; cursor: pointer; border: 1px solid #1d3557; }
.btn-del { background: #fff; color: #c92f3c; border-color: #c92f3c; }
.btn-del.disabled, .btn-del:disabled { opacity: .35; cursor: not-allowed; }
.btn-query { background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; font-size: 12px; padding: .25rem .6rem; }
.result { font-size: .82rem; font-weight: 600; min-height: 1.2em; margin-bottom: .2rem; }
.result.yes { color: #c97f0a; }
.result.no { color: #0a7d33; }
.result.fp { color: #c92f3c; }
.stat { font-size: .75rem; color: #666; }
.legend { margin-top: .7rem; display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
.legend-note { font-size: .78rem; color: #666; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Universal-style hash with 32-bit mixing (no crypto needed).
function hashStr(str, seed) {
let h = seed ^ 0xdeadbeef;
for (let i = 0; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 0x9e3779b9);
h ^= h >>> 16;
}
return (h >>> 0);
}
// 8-bit fingerprint for cuckoo filter (never zero so empty slot = 0 is safe).
function fingerprint(str) {
return (hashStr(str, 0xabcdef01) & 0xff) || 1;
}
const M = 32; // bit-array / counter-array size
const K = 3; // number of hash functions
const BUCKETS = 16; // cuckoo table buckets
const SLOTS = 4; // slots per bucket
// Filter state: all start empty.
let classicBits = new Uint8Array(M);
let countingArr = new Uint8Array(M);
let cuckooTable = Array.from({ length: BUCKETS }, () => Array(SLOTS).fill(0));
let blockedBits = new Uint8Array(M);
// Each block is half the array, corresponding to one cache line.
const BLOCK_SIZE = M / 2;
// Track what was actually inserted so we can label false positives.
const insertedSet = new Set();
const deletedFromCounting = new Set();
const deletedFromCuckoo = new Set();
let queryTarget = '';
function getHashes(item) {
const h = [];
for (let i = 0; i < K; i++) h.push(hashStr(item, i * 0x1234567 + 0xabcd) % M);
return h;
}
function getBlockHashes(item) {
// Map item to one of 2 blocks, then hash within that block.
const block = hashStr(item, 0x99887766) % 2;
const base = block * BLOCK_SIZE;
const h = [];
for (let i = 0; i < K; i++) h.push(base + hashStr(item, i * 0x1234 + 0xbeef) % BLOCK_SIZE);
return h;
}
function insertClassic(item) {
for (const b of getHashes(item)) classicBits[b] = 1;
}
function queryClassic(item) {
return getHashes(item).every(b => classicBits[b]);
}
function insertCounting(item) {
for (const b of getHashes(item)) if (countingArr[b] < 15) countingArr[b]++;
}
function deleteCounting(item) {
// Safe decrement: never go below zero.
for (const b of getHashes(item)) if (countingArr[b] > 0) countingArr[b]--;
}
function queryCounting(item) {
return getHashes(item).every(b => countingArr[b] > 0);
}
function cuckooInsert(item) {
const fp = fingerprint(item);
const h1 = hashStr(item, 0x1111) % BUCKETS;
const h2 = (h1 ^ hashStr(fp + '', 0x2222)) % BUCKETS;
// Try both candidate buckets before kicking.
for (const b of [h1, h2]) {
for (let s = 0; s < SLOTS; s++) {
if (!cuckooTable[b][s]) { cuckooTable[b][s] = fp; return true; }
}
}
// Kick-and-relocate loop (max 20 rounds).
let cur = h1; let curFp = fp;
for (let i = 0; i < 20; i++) {
const s = Math.floor(Math.random() * SLOTS);
const evicted = cuckooTable[cur][s];
cuckooTable[cur][s] = curFp;
curFp = evicted;
cur = (cur ^ hashStr(curFp + '', 0x2222)) % BUCKETS;
for (let ss = 0; ss < SLOTS; ss++) {
if (!cuckooTable[cur][ss]) { cuckooTable[cur][ss] = curFp; return true; }
}
}
return false; // Table too full — in practice resize before this point.
}
function cuckooDelete(item) {
const fp = fingerprint(item);
const h1 = hashStr(item, 0x1111) % BUCKETS;
const h2 = (h1 ^ hashStr(fp + '', 0x2222)) % BUCKETS;
for (const b of [h1, h2]) {
for (let s = 0; s < SLOTS; s++) {
if (cuckooTable[b][s] === fp) { cuckooTable[b][s] = 0; return; }
}
}
}
function cuckooQuery(item) {
const fp = fingerprint(item);
const h1 = hashStr(item, 0x1111) % BUCKETS;
const h2 = (h1 ^ hashStr(fp + '', 0x2222)) % BUCKETS;
for (const b of [h1, h2]) {
for (let s = 0; s < SLOTS; s++) if (cuckooTable[b][s] === fp) return true;
}
return false;
}
function insertBlocked(item) {
for (const b of getBlockHashes(item)) blockedBits[b] = 1;
}
function queryBlocked(item) {
return getBlockHashes(item).every(b => blockedBits[b]);
}
// Render a bit or counter array onto a canvas element.
function drawBits(canvasId, arr, highlightSet) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const cellW = W / arr.length;
for (let i = 0; i < arr.length; i++) {
const lit = arr[i] > 0;
const hi = highlightSet && highlightSet.has(i);
ctx.fillStyle = hi ? '#e63946' : (lit ? '#1d3557' : '#d0d7df');
ctx.fillRect(i * cellW + 1, 4, cellW - 2, H - 8);
if (arr[i] > 1 && arr[i] < 10) {
ctx.fillStyle = '#fff';
ctx.font = 'bold 8px ui-monospace';
ctx.textAlign = 'center';
ctx.fillText(arr[i], i * cellW + cellW / 2, H / 2 + 3);
}
}
}
function drawCuckoo(highlightFp) {
const container = document.getElementById('buckets-cuckoo');
if (!container) return;
container.innerHTML = '';
for (let b = 0; b < BUCKETS; b++) {
for (let s = 0; s < SLOTS; s++) {
const fp = cuckooTable[b][s];
const div = document.createElement('div');
div.className = 'bucket' + (fp ? ' used' : '');
if (fp) {
div.textContent = fp.toString(16).padStart(2, '0').toUpperCase();
if (highlightFp && fp === highlightFp) div.style.background = '#e63946';
}
container.appendChild(div);
}
}
}
function renderAll(queryItem) {
const classicHi = queryItem ? new Set(getHashes(queryItem)) : null;
const countingHi = queryItem ? new Set(getHashes(queryItem)) : null;
const blockedHi = queryItem ? new Set(getBlockHashes(queryItem)) : null;
const cuckooHi = queryItem ? fingerprint(queryItem) : null;
drawBits('bits-classic', classicBits, classicHi);
drawBits('bits-counting', countingArr, countingHi);
drawCuckoo(cuckooHi);
drawBits('bits-blocked', blockedBits, blockedHi);
}
function setResult(id, text, cls) {
const el = document.getElementById(id);
if (el) { el.textContent = text; el.className = 'result ' + cls; }
}
function setStat(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
function setMsg(text, isErr) {
const el = document.getElementById('msg');
if (el) { el.textContent = text; el.className = isErr ? 'msg err' : 'msg'; }
}
function countBits(arr) { return arr.reduce((a, b) => a + (b > 0 ? 1 : 0), 0); }
function countCuckooBuckets() {
let n = 0; for (const row of cuckooTable) n += row.filter(x => x).length; return n;
}
function updateStats() {
const filled = countBits(classicBits);
setStat('stat-classic', i18nFmt('%n% / %m% bits set', { n: filled, m: M }));
const cFilled = countBits(countingArr);
setStat('stat-counting', i18nFmt('%n% / %m% counters non-zero', { n: cFilled, m: M }));
const cuckooFilled = countCuckooBuckets();
setStat('stat-cuckoo', i18nFmt('%n% / %t% fingerprint slots used', { n: cuckooFilled, t: BUCKETS * SLOTS }));
const bFilled = countBits(blockedBits);
setStat('stat-blocked', i18nFmt('%n% / %m% bits set', { n: bFilled, m: M }));
}
document.getElementById('btn-insert').addEventListener('click', () => {
const val = document.getElementById('item-input').value.trim();
if (!val) { setMsg('Please type an item first.', true); return; }
if (insertedSet.has(val)) { setMsg(i18nFmt(''%item%' was already inserted.', { item: val }), true); return; }
insertedSet.add(val);
deletedFromCounting.delete(val);
deletedFromCuckoo.delete(val);
insertClassic(val);
insertCounting(val);
cuckooInsert(val);
insertBlocked(val);
setMsg(i18nFmt(''%item%' inserted into all four filters.', { item: val }), false);
queryTarget = '';
setResult('result-classic', '', '');
setResult('result-counting', '', '');
setResult('result-cuckoo', '', '');
setResult('result-blocked', '', '');
renderAll(null);
updateStats();
});
document.getElementById('del-counting').addEventListener('click', () => {
const val = document.getElementById('item-input').value.trim();
if (!val) { setMsg('Please type an item first.', true); return; }
if (!insertedSet.has(val) || deletedFromCounting.has(val)) {
setMsg(i18nFmt(''%item%' was not inserted (or already deleted from this filter).', { item: val }), true); return;
}
deletedFromCounting.add(val);
deleteCounting(val);
setMsg(i18nFmt(''%item%' deleted from counting filter.', { item: val }), false);
renderAll(null);
updateStats();
});
document.getElementById('del-cuckoo').addEventListener('click', () => {
const val = document.getElementById('item-input').value.trim();
if (!val) { setMsg('Please type an item first.', true); return; }
if (!insertedSet.has(val) || deletedFromCuckoo.has(val)) {
setMsg(i18nFmt(''%item%' was not inserted (or already deleted from this filter).', { item: val }), true); return;
}
deletedFromCuckoo.add(val);
cuckooDelete(val);
setMsg(i18nFmt(''%item%' deleted from cuckoo filter.', { item: val }), false);
renderAll(null);
updateStats();
});
function doQuery(filterId) {
const val = document.getElementById('item-input').value.trim();
if (!val) { setMsg('Please type an item first.', true); return; }
queryTarget = val;
renderAll(val);
const wasInserted = insertedSet.has(val);
if (filterId === 'classic') {
const found = queryClassic(val);
if (!wasInserted && found) setResult('result-classic', 'FALSE POSITIVE — not inserted but filter says yes!', 'fp');
else if (found) setResult('result-classic', 'Probably present', 'yes');
else setResult('result-classic', 'Definitely absent', 'no');
} else if (filterId === 'counting') {
const found = queryCounting(val);
const deleted = deletedFromCounting.has(val);
if (!wasInserted && found) setResult('result-counting', 'FALSE POSITIVE — not inserted but filter says yes!', 'fp');
else if (wasInserted && deleted && !found) setResult('result-counting', 'Absent (correctly deleted)', 'no');
else if (found) setResult('result-counting', 'Probably present', 'yes');
else setResult('result-counting', 'Definitely absent', 'no');
} else if (filterId === 'cuckoo') {
const found = cuckooQuery(val);
const deleted = deletedFromCuckoo.has(val);
if (!wasInserted && found) setResult('result-cuckoo', 'FALSE POSITIVE — not inserted but filter says yes!', 'fp');
else if (wasInserted && deleted && !found) setResult('result-cuckoo', 'Absent (correctly deleted)', 'no');
else if (found) setResult('result-cuckoo', 'Probably present', 'yes');
else setResult('result-cuckoo', 'Definitely absent', 'no');
} else if (filterId === 'blocked') {
const found = queryBlocked(val);
if (!wasInserted && found) setResult('result-blocked', 'FALSE POSITIVE — not inserted but filter says yes!', 'fp');
else if (found) setResult('result-blocked', 'Probably present', 'yes');
else setResult('result-blocked', 'Definitely absent', 'no');
}
}
document.getElementById('query-classic').addEventListener('click', () => doQuery('classic'));
document.getElementById('query-counting').addEventListener('click', () => doQuery('counting'));
document.getElementById('query-cuckoo').addEventListener('click', () => doQuery('cuckoo'));
document.getElementById('query-blocked').addEventListener('click', () => doQuery('blocked'));
document.getElementById('btn-reset').addEventListener('click', () => {
classicBits = new Uint8Array(M);
countingArr = new Uint8Array(M);
cuckooTable = Array.from({ length: BUCKETS }, () => Array(SLOTS).fill(0));
blockedBits = new Uint8Array(M);
insertedSet.clear();
deletedFromCounting.clear();
deletedFromCuckoo.clear();
queryTarget = '';
['result-classic','result-counting','result-cuckoo','result-blocked'].forEach(id => setResult(id,'',''));
['stat-classic','stat-counting','stat-cuckoo','stat-blocked'].forEach(id => setStat(id,''));
setMsg('All filters reset.', false);
renderAll(null);
});
renderAll(null);
updateStats();
<!-- Bloom filter variants comparison demo -->
<div class="demo-wrap">
<div class="controls">
<label for="item-input">Item to insert / delete / query:</label>
<div class="input-row">
<input id="item-input" type="text" placeholder="e.g. alice" maxlength="20" />
<button id="btn-insert" type="button">Insert</button>
</div>
<div id="msg" class="msg"></div>
</div>
<div class="filters-grid">
<div class="filter-card" id="card-classic">
<div class="filter-name">Classic Bloom</div>
<div class="filter-desc">k=3 hash functions, m=32 bits. No delete.</div>
<canvas class="bits-canvas" id="bits-classic" width="320" height="36" title="Bit array (dark = 1)"></canvas>
<div class="filter-actions">
<button class="btn-del disabled" id="del-classic" type="button" disabled title="This filter does not support deletion">Delete</button>
<button class="btn-query" id="query-classic" type="button">Query</button>
</div>
<div class="result" id="result-classic"></div>
<div class="stat" id="stat-classic"></div>
</div>
<div class="filter-card" id="card-counting">
<div class="filter-name">Counting Filter</div>
<div class="filter-desc">k=3 counters (4-bit each), m=32 slots. Delete supported.</div>
<canvas class="bits-canvas" id="bits-counting" width="320" height="36" title="Counter array (dark = non-zero, number = count)"></canvas>
<div class="filter-actions">
<button class="btn-del" id="del-counting" type="button">Delete</button>
<button class="btn-query" id="query-counting" type="button">Query</button>
</div>
<div class="result" id="result-counting"></div>
<div class="stat" id="stat-counting"></div>
</div>
<div class="filter-card" id="card-cuckoo">
<div class="filter-name">Cuckoo Filter</div>
<div class="filter-desc">8-bit fingerprints, 16 buckets × 4 slots. Delete supported.</div>
<div class="cuckoo-buckets" id="buckets-cuckoo" title="Fingerprint slots (hex, red = queried fingerprint)"></div>
<div class="filter-actions">
<button class="btn-del" id="del-cuckoo" type="button">Delete</button>
<button class="btn-query" id="query-cuckoo" type="button">Query</button>
</div>
<div class="result" id="result-cuckoo"></div>
<div class="stat" id="stat-cuckoo"></div>
</div>
<div class="filter-card" id="card-blocked">
<div class="filter-name">Blocked Bloom</div>
<div class="filter-desc">k=3 per block, 2 blocks of 16 bits. No delete, 1 cache line.</div>
<canvas class="bits-canvas" id="bits-blocked" width="320" height="36" title="Blocked bit array (two cache-line blocks)"></canvas>
<div class="filter-actions">
<button class="btn-del disabled" id="del-blocked" type="button" disabled title="This filter does not support deletion">Delete</button>
<button class="btn-query" id="query-blocked" type="button">Query</button>
</div>
<div class="result" id="result-blocked"></div>
<div class="stat" id="stat-blocked"></div>
</div>
</div>
<div class="legend">
<button id="btn-reset" type="button" class="ghost">Reset all</button>
<span class="legend-note">Red = queried positions · Orange = probably present · Green = absent · Crimson = false positive</span>
</div>
</div>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; font-size: 14px; }
.demo-wrap { padding: .6rem .5rem; }
.controls { margin-bottom: .8rem; }
.controls label { font-weight: 600; display: block; margin-bottom: .3rem; }
.input-row { display: flex; gap: .4rem; margin-bottom: .35rem; }
#item-input { flex: 1; padding: .4rem .6rem; border: 1px solid #bbb; border-radius: 6px; font-size: 14px; }
.msg { font-size: .85rem; color: #666; min-height: 1.2em; }
.msg.err { color: #c92f3c; }
.filters-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
@media (max-width: 500px) { .filters-grid { grid-template-columns: 1fr; } }
.filter-card { border: 1px solid #dde3ea; border-radius: 10px; padding: .6rem .7rem; background: #f9fafb; }
.filter-name { font-weight: 700; font-size: .92rem; margin-bottom: .15rem; }
.filter-desc { font-size: .76rem; color: #555; margin-bottom: .4rem; line-height: 1.4; }
.bits-canvas { display: block; width: 100%; height: 36px; border-radius: 4px; margin-bottom: .4rem; background: #e8eef3; }
.cuckoo-buckets { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: .4rem; min-height: 36px; align-content: flex-start; }
.bucket { width: 26px; height: 26px; border-radius: 4px; display: flex; align-items: center; justify-content: center; font: 600 9px ui-monospace, monospace; background: #e8eef3; border: 1px solid #cdd9e3; color: #1d3557; }
.bucket.used { background: #1d3557; color: #fff; border-color: #1d3557; }
.filter-actions { display: flex; gap: .35rem; margin-bottom: .35rem; }
button { font: 600 13px system-ui; padding: .35rem .7rem; border-radius: 6px; cursor: pointer; border: 1px solid #1d3557; }
.btn-del { background: #fff; color: #c92f3c; border-color: #c92f3c; }
.btn-del.disabled, .btn-del:disabled { opacity: .35; cursor: not-allowed; }
.btn-query { background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; font-size: 12px; padding: .25rem .6rem; }
.result { font-size: .82rem; font-weight: 600; min-height: 1.2em; margin-bottom: .2rem; }
.result.yes { color: #c97f0a; }
.result.no { color: #0a7d33; }
.result.fp { color: #c92f3c; }
.stat { font-size: .75rem; color: #666; }
.legend { margin-top: .7rem; display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
.legend-note { font-size: .78rem; color: #666; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Universal-style hash with 32-bit mixing (no crypto needed).
function hashStr(str, seed) {
let h = seed ^ 0xdeadbeef;
for (let i = 0; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 0x9e3779b9);
h ^= h >>> 16;
}
return (h >>> 0);
}
// 8-bit fingerprint for cuckoo filter (never zero so empty slot = 0 is safe).
function fingerprint(str) {
return (hashStr(str, 0xabcdef01) & 0xff) || 1;
}
const M = 32; // bit-array / counter-array size
const K = 3; // number of hash functions
const BUCKETS = 16; // cuckoo table buckets
const SLOTS = 4; // slots per bucket
// Filter state: all start empty.
let classicBits = new Uint8Array(M);
let countingArr = new Uint8Array(M);
let cuckooTable = Array.from({ length: BUCKETS }, () => Array(SLOTS).fill(0));
let blockedBits = new Uint8Array(M);
// Each block is half the array, corresponding to one cache line.
const BLOCK_SIZE = M / 2;
// Track what was actually inserted so we can label false positives.
const insertedSet = new Set();
const deletedFromCounting = new Set();
const deletedFromCuckoo = new Set();
let queryTarget = '';
function getHashes(item) {
const h = [];
for (let i = 0; i < K; i++) h.push(hashStr(item, i * 0x1234567 + 0xabcd) % M);
return h;
}
function getBlockHashes(item) {
// Map item to one of 2 blocks, then hash within that block.
const block = hashStr(item, 0x99887766) % 2;
const base = block * BLOCK_SIZE;
const h = [];
for (let i = 0; i < K; i++) h.push(base + hashStr(item, i * 0x1234 + 0xbeef) % BLOCK_SIZE);
return h;
}
function insertClassic(item) {
for (const b of getHashes(item)) classicBits[b] = 1;
}
function queryClassic(item) {
return getHashes(item).every(b => classicBits[b]);
}
function insertCounting(item) {
for (const b of getHashes(item)) if (countingArr[b] < 15) countingArr[b]++;
}
function deleteCounting(item) {
// Safe decrement: never go below zero.
for (const b of getHashes(item)) if (countingArr[b] > 0) countingArr[b]--;
}
function queryCounting(item) {
return getHashes(item).every(b => countingArr[b] > 0);
}
function cuckooInsert(item) {
const fp = fingerprint(item);
const h1 = hashStr(item, 0x1111) % BUCKETS;
const h2 = (h1 ^ hashStr(fp + '', 0x2222)) % BUCKETS;
// Try both candidate buckets before kicking.
for (const b of [h1, h2]) {
for (let s = 0; s < SLOTS; s++) {
if (!cuckooTable[b][s]) { cuckooTable[b][s] = fp; return true; }
}
}
// Kick-and-relocate loop (max 20 rounds).
let cur = h1; let curFp = fp;
for (let i = 0; i < 20; i++) {
const s = Math.floor(Math.random() * SLOTS);
const evicted = cuckooTable[cur][s];
cuckooTable[cur][s] = curFp;
curFp = evicted;
cur = (cur ^ hashStr(curFp + '', 0x2222)) % BUCKETS;
for (let ss = 0; ss < SLOTS; ss++) {
if (!cuckooTable[cur][ss]) { cuckooTable[cur][ss] = curFp; return true; }
}
}
return false; // Table too full — in practice resize before this point.
}
function cuckooDelete(item) {
const fp = fingerprint(item);
const h1 = hashStr(item, 0x1111) % BUCKETS;
const h2 = (h1 ^ hashStr(fp + '', 0x2222)) % BUCKETS;
for (const b of [h1, h2]) {
for (let s = 0; s < SLOTS; s++) {
if (cuckooTable[b][s] === fp) { cuckooTable[b][s] = 0; return; }
}
}
}
function cuckooQuery(item) {
const fp = fingerprint(item);
const h1 = hashStr(item, 0x1111) % BUCKETS;
const h2 = (h1 ^ hashStr(fp + '', 0x2222)) % BUCKETS;
for (const b of [h1, h2]) {
for (let s = 0; s < SLOTS; s++) if (cuckooTable[b][s] === fp) return true;
}
return false;
}
function insertBlocked(item) {
for (const b of getBlockHashes(item)) blockedBits[b] = 1;
}
function queryBlocked(item) {
return getBlockHashes(item).every(b => blockedBits[b]);
}
// Render a bit or counter array onto a canvas element.
function drawBits(canvasId, arr, highlightSet) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const cellW = W / arr.length;
for (let i = 0; i < arr.length; i++) {
const lit = arr[i] > 0;
const hi = highlightSet && highlightSet.has(i);
ctx.fillStyle = hi ? '#e63946' : (lit ? '#1d3557' : '#d0d7df');
ctx.fillRect(i * cellW + 1, 4, cellW - 2, H - 8);
if (arr[i] > 1 && arr[i] < 10) {
ctx.fillStyle = '#fff';
ctx.font = 'bold 8px ui-monospace';
ctx.textAlign = 'center';
ctx.fillText(arr[i], i * cellW + cellW / 2, H / 2 + 3);
}
}
}
function drawCuckoo(highlightFp) {
const container = document.getElementById('buckets-cuckoo');
if (!container) return;
container.innerHTML = '';
for (let b = 0; b < BUCKETS; b++) {
for (let s = 0; s < SLOTS; s++) {
const fp = cuckooTable[b][s];
const div = document.createElement('div');
div.className = 'bucket' + (fp ? ' used' : '');
if (fp) {
div.textContent = fp.toString(16).padStart(2, '0').toUpperCase();
if (highlightFp && fp === highlightFp) div.style.background = '#e63946';
}
container.appendChild(div);
}
}
}
function renderAll(queryItem) {
const classicHi = queryItem ? new Set(getHashes(queryItem)) : null;
const countingHi = queryItem ? new Set(getHashes(queryItem)) : null;
const blockedHi = queryItem ? new Set(getBlockHashes(queryItem)) : null;
const cuckooHi = queryItem ? fingerprint(queryItem) : null;
drawBits('bits-classic', classicBits, classicHi);
drawBits('bits-counting', countingArr, countingHi);
drawCuckoo(cuckooHi);
drawBits('bits-blocked', blockedBits, blockedHi);
}
function setResult(id, text, cls) {
const el = document.getElementById(id);
if (el) { el.textContent = text; el.className = 'result ' + cls; }
}
function setStat(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
function setMsg(text, isErr) {
const el = document.getElementById('msg');
if (el) { el.textContent = text; el.className = isErr ? 'msg err' : 'msg'; }
}
function countBits(arr) { return arr.reduce((a, b) => a + (b > 0 ? 1 : 0), 0); }
function countCuckooBuckets() {
let n = 0; for (const row of cuckooTable) n += row.filter(x => x).length; return n;
}
function updateStats() {
const filled = countBits(classicBits);
setStat('stat-classic', i18nFmt('%n% / %m% bits set', { n: filled, m: M }));
const cFilled = countBits(countingArr);
setStat('stat-counting', i18nFmt('%n% / %m% counters non-zero', { n: cFilled, m: M }));
const cuckooFilled = countCuckooBuckets();
setStat('stat-cuckoo', i18nFmt('%n% / %t% fingerprint slots used', { n: cuckooFilled, t: BUCKETS * SLOTS }));
const bFilled = countBits(blockedBits);
setStat('stat-blocked', i18nFmt('%n% / %m% bits set', { n: bFilled, m: M }));
}
document.getElementById('btn-insert').addEventListener('click', () => {
const val = document.getElementById('item-input').value.trim();
if (!val) { setMsg('Please type an item first.', true); return; }
if (insertedSet.has(val)) { setMsg(i18nFmt(''%item%' was already inserted.', { item: val }), true); return; }
insertedSet.add(val);
deletedFromCounting.delete(val);
deletedFromCuckoo.delete(val);
insertClassic(val);
insertCounting(val);
cuckooInsert(val);
insertBlocked(val);
setMsg(i18nFmt(''%item%' inserted into all four filters.', { item: val }), false);
queryTarget = '';
setResult('result-classic', '', '');
setResult('result-counting', '', '');
setResult('result-cuckoo', '', '');
setResult('result-blocked', '', '');
renderAll(null);
updateStats();
});
document.getElementById('del-counting').addEventListener('click', () => {
const val = document.getElementById('item-input').value.trim();
if (!val) { setMsg('Please type an item first.', true); return; }
if (!insertedSet.has(val) || deletedFromCounting.has(val)) {
setMsg(i18nFmt(''%item%' was not inserted (or already deleted from this filter).', { item: val }), true); return;
}
deletedFromCounting.add(val);
deleteCounting(val);
setMsg(i18nFmt(''%item%' deleted from counting filter.', { item: val }), false);
renderAll(null);
updateStats();
});
document.getElementById('del-cuckoo').addEventListener('click', () => {
const val = document.getElementById('item-input').value.trim();
if (!val) { setMsg('Please type an item first.', true); return; }
if (!insertedSet.has(val) || deletedFromCuckoo.has(val)) {
setMsg(i18nFmt(''%item%' was not inserted (or already deleted from this filter).', { item: val }), true); return;
}
deletedFromCuckoo.add(val);
cuckooDelete(val);
setMsg(i18nFmt(''%item%' deleted from cuckoo filter.', { item: val }), false);
renderAll(null);
updateStats();
});
function doQuery(filterId) {
const val = document.getElementById('item-input').value.trim();
if (!val) { setMsg('Please type an item first.', true); return; }
queryTarget = val;
renderAll(val);
const wasInserted = insertedSet.has(val);
if (filterId === 'classic') {
const found = queryClassic(val);
if (!wasInserted && found) setResult('result-classic', 'FALSE POSITIVE — not inserted but filter says yes!', 'fp');
else if (found) setResult('result-classic', 'Probably present', 'yes');
else setResult('result-classic', 'Definitely absent', 'no');
} else if (filterId === 'counting') {
const found = queryCounting(val);
const deleted = deletedFromCounting.has(val);
if (!wasInserted && found) setResult('result-counting', 'FALSE POSITIVE — not inserted but filter says yes!', 'fp');
else if (wasInserted && deleted && !found) setResult('result-counting', 'Absent (correctly deleted)', 'no');
else if (found) setResult('result-counting', 'Probably present', 'yes');
else setResult('result-counting', 'Definitely absent', 'no');
} else if (filterId === 'cuckoo') {
const found = cuckooQuery(val);
const deleted = deletedFromCuckoo.has(val);
if (!wasInserted && found) setResult('result-cuckoo', 'FALSE POSITIVE — not inserted but filter says yes!', 'fp');
else if (wasInserted && deleted && !found) setResult('result-cuckoo', 'Absent (correctly deleted)', 'no');
else if (found) setResult('result-cuckoo', 'Probably present', 'yes');
else setResult('result-cuckoo', 'Definitely absent', 'no');
} else if (filterId === 'blocked') {
const found = queryBlocked(val);
if (!wasInserted && found) setResult('result-blocked', 'FALSE POSITIVE — not inserted but filter says yes!', 'fp');
else if (found) setResult('result-blocked', 'Probably present', 'yes');
else setResult('result-blocked', 'Definitely absent', 'no');
}
}
document.getElementById('query-classic').addEventListener('click', () => doQuery('classic'));
document.getElementById('query-counting').addEventListener('click', () => doQuery('counting'));
document.getElementById('query-cuckoo').addEventListener('click', () => doQuery('cuckoo'));
document.getElementById('query-blocked').addEventListener('click', () => doQuery('blocked'));
document.getElementById('btn-reset').addEventListener('click', () => {
classicBits = new Uint8Array(M);
countingArr = new Uint8Array(M);
cuckooTable = Array.from({ length: BUCKETS }, () => Array(SLOTS).fill(0));
blockedBits = new Uint8Array(M);
insertedSet.clear();
deletedFromCounting.clear();
deletedFromCuckoo.clear();
queryTarget = '';
['result-classic','result-counting','result-cuckoo','result-blocked'].forEach(id => setResult(id,'',''));
['stat-classic','stat-counting','stat-cuckoo','stat-blocked'].forEach(id => setStat(id,''));
setMsg('All filters reset.', false);
renderAll(null);
});
renderAll(null);
updateStats();
Comments
Loading comments...