Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { highlight, type HighlightedCode } from "codehike/code";
import { BloomDemoRunnerClient, type RunnerStep } from "./BloomDemoRunnerClient";

const SOURCE = `import { create, isDatabaseSuccess } from "create-db";
import { Client } from "pg";

console.log("Provisioning a temporary Prisma Postgres database (1h TTL)...");
const db = await create({ ttl: "1h" });
if (!isDatabaseSuccess(db)) throw new Error(db.message);
console.log(\` claim URL: \${db.claimUrl}\`);

const client = new Client({ connectionString: db.connectionString! });
await client.connect();

await client.query(\`CREATE EXTENSION IF NOT EXISTS bloom\`);
await client.query(\`
DROP TABLE IF EXISTS cache_entries;
CREATE TABLE cache_entries (
id BIGSERIAL PRIMARY KEY,
tenant_id TEXT, user_id TEXT, endpoint TEXT,
locale TEXT, region TEXT, api_version INT,
payload JSONB
);
\`);

console.log(\`Seeding \${N.toLocaleString()} rows...\`);
for (let start = 0; start < N; start += BATCH) {
// build BATCH rows mixing tenants / users / endpoints / locales / regions / versions
await client.query(\`INSERT INTO cache_entries (...) VALUES \${placeholders}\`, params);
}
await client.query(\`ANALYZE cache_entries\`);

// A. Six B-tree indexes (one per column)
for (const c of COLS) {
await client.query(\`CREATE INDEX btree_\${c} ON cache_entries (\${c})\`);
}
const btreeMB = await totalIndexMB();
const btreeMs = await runLookups();

// B. One bloom index covering all six columns
await client.query(\`
CREATE INDEX cache_bloom_idx ON cache_entries
USING bloom (\${COLS.join(", ")})
\`);
const bloomMB = await totalIndexMB();
const bloomMs = await runLookups();

const shrink = ((1 - bloomMB / btreeMB) * 100).toFixed(0);
console.log(
\`Bloom index is \${shrink}% smaller \` +
\`(\${bloomMB.toFixed(1)} MB vs \${btreeMB.toFixed(1)} MB)\`,
);`;

const STEPS: RunnerStep[] = [
{
title: "Provision DB",
caption:
"Spin up a temporary Prisma Postgres database with a 1 hour TTL and connect over pg.",
lines: { from: 1, to: 10 },
output: [
"Provisioning a temporary Prisma Postgres database (1h TTL)...",
" claim URL: https://create-db.prisma.io/claim?projectID=...",
],
},
{
title: "Schema + bloom extension",
caption:
"Enable the bloom extension once per database, then create the wide cache_entries table.",
lines: { from: 12, to: 21 },
output: ["Creating cache_entries table and enabling bloom extension..."],
},
{
title: "Seed 10,000 rows",
caption:
"Insert a mix of tenants, users, endpoints, locales, regions, and api versions so the lookups are realistic.",
lines: { from: 23, to: 28 },
output: ["Seeding 10,000 rows...", " seeded in 1.2s"],
},
{
title: "A: Six B-tree indexes",
caption:
"One B-tree per filterable column. Measure the total index size and the time for three lookups.",
lines: { from: 30, to: 35 },
output: [
"",
"A. Six B-tree indexes (one per column)...",
" index size: 0.5 MB",
" 3 lookups: 306.5 ms",
],
},
{
title: "B: One bloom index",
caption:
"Drop the B-trees and create a single bloom index spanning all six columns. Same three lookups.",
lines: { from: 37, to: 43 },
output: [
"",
"B. One bloom index (all six columns)...",
" index size: 0.2 MB",
" 3 lookups: 302.7 ms",
Comment thread
ankur-arch marked this conversation as resolved.
],
},
{
title: "Compare",
caption:
"Print the difference. The bloom index is much smaller, and it covers any subset of those six columns.",
lines: { from: 45, to: 49 },
output: [
"",
" Bloom index is 70% smaller (0.2 MB vs 0.5 MB),",
Comment thread
ankur-arch marked this conversation as resolved.
Outdated
" and one index covers any subset of those six columns.",
],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
];

export async function BloomDemoRunner() {
const baseCode = (await highlight(
{ value: SOURCE, lang: "typescript", meta: "" },
"github-from-css",
)) as HighlightedCode;
return <BloomDemoRunnerClient baseCode={baseCode} steps={STEPS} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import { InnerLine, Pre, type AnnotationHandler, type HighlightedCode } from "codehike/code";
import { Check, ChevronLeft, ChevronRight, Copy, Pause, Play } from "lucide-react";

export type RunnerStep = {
title: string;
caption: string;
lines: { from: number; to: number };
output: string[];
};

type Props = {
baseCode: HighlightedCode;
steps: RunnerStep[];
};

const STEP_HOLD_MS = 6500;

const markHandler: AnnotationHandler = {
name: "mark",
AnnotatedLine: ({ annotation, ...props }) => (
<InnerLine merge={props} data-mark={annotation.query || "active"} />
),
};

const handlers = [markHandler];

function codeForStep(base: HighlightedCode, step: RunnerStep): HighlightedCode {
const lineMarks = [];
for (let n = step.lines.from; n <= step.lines.to; n += 1) {
lineMarks.push({
name: "mark",
query: "active",
fromLineNumber: n,
toLineNumber: n,
});
}
return {
...base,
annotations: [...base.annotations.filter((a) => a.name !== "mark"), ...lineMarks],
};
}

export function BloomDemoRunnerClient({ baseCode, steps }: Props) {
const [stepIndex, setStepIndex] = useState(0);
const [playing, setPlaying] = useState(true);
const [inView, setInView] = useState(false);
const [copied, setCopied] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const codeScrollRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<HTMLDivElement>(null);
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
const el = containerRef.current;
if (!el || typeof IntersectionObserver === "undefined") {
setInView(true);
return;
}
const obs = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), {
threshold: 0.2,
});
obs.observe(el);
return () => obs.disconnect();
}, []);

useEffect(() => {
if (!playing || !inView) return;
const id = setInterval(() => {
setStepIndex((i) => (i + 1) % steps.length);
}, STEP_HOLD_MS);
return () => clearInterval(id);
}, [playing, inView, steps.length]);

const step = steps[stepIndex];
const code = useMemo(() => codeForStep(baseCode, step), [baseCode, step]);

useEffect(() => {
const codeEl = codeScrollRef.current;
if (codeEl) {
const highlighted = codeEl.querySelector<HTMLElement>('[data-mark="active"]');
if (highlighted) {
const parent = codeEl;
const top = highlighted.offsetTop - parent.offsetTop;
parent.scrollTo({ top: Math.max(0, top - 20), behavior: "smooth" });
}
}
const termEl = terminalRef.current;
if (termEl) {
const active = termEl.querySelector<HTMLElement>('[data-step-state="active"]');
if (active) {
const top = active.offsetTop - termEl.offsetTop;
termEl.scrollTo({ top: Math.max(0, top - 20), behavior: "smooth" });
}
}
}, [stepIndex]);

function goTo(index: number) {
setPlaying(false);
setStepIndex(((index % steps.length) + steps.length) % steps.length);
}

const allOutput = steps.flatMap((s, i) => s.output.map((line) => ({ line, stepIdx: i })));

function copyOutput() {
const text = allOutput.map((e) => e.line).join("\n");
if (!navigator.clipboard) return;
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
copyTimeoutRef.current = setTimeout(() => setCopied(false), 1600);
});
}
Comment thread
ankur-arch marked this conversation as resolved.

useEffect(() => {
return () => {
if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
};
}, []);

return (
<div ref={containerRef} className="runner not-prose">
<div className="runner-header">
<span className="runner-filename">
<span className="runner-filename-dots" aria-hidden="true">
<span />
<span />
<span />
</span>
index.ts
</span>
<span className="runner-step-counter">
Step {stepIndex + 1} of {steps.length}
<span className="runner-step-counter-label"> &middot; {step.title}</span>
</span>
<div className="runner-nav">
<button
type="button"
className="runner-toggle"
onClick={() => goTo(stepIndex - 1)}
aria-label="Previous step"
>
<ChevronLeft size={16} />
</button>
<button
type="button"
className="runner-toggle"
onClick={() => setPlaying((p) => !p)}
aria-label={playing ? "Pause demo" : "Play demo"}
>
{playing ? <Pause size={16} /> : <Play size={16} />}
</button>
<button
type="button"
className="runner-toggle"
onClick={() => goTo(stepIndex + 1)}
aria-label="Next step"
>
<ChevronRight size={16} />
</button>
</div>
</div>

<div className="runner-steps" role="tablist" aria-label="Demo steps">
{steps.map((s, i) => (
<button
key={s.title}
type="button"
role="tab"
aria-selected={i === stepIndex}
data-active={i === stepIndex ? "true" : undefined}
className="runner-step-pill"
onClick={() => goTo(i)}
>
<span className="runner-step-pill-num">{i + 1}</span>
<span className="runner-step-pill-label">{s.title}</span>
</button>
))}
</div>

<div className="runner-caption">{step.caption}</div>

<div className="runner-body">
<div className="runner-pane runner-pane-code">
<div className="runner-pane-label">
<span>index.ts</span>
</div>
<div className="runner-code" ref={codeScrollRef}>
<Pre code={code} handlers={handlers} />
</div>
</div>
<div className="runner-pane runner-pane-terminal">
<div className="runner-pane-label runner-pane-label-terminal">
<span>terminal output</span>
<button
type="button"
className="runner-copy"
onClick={copyOutput}
aria-label={copied ? "Copied terminal output" : "Copy terminal output"}
>
{copied ? <Check size={12} /> : <Copy size={12} />}
<span>{copied ? "Copied" : "Copy"}</span>
</button>
</div>
<div className="runner-terminal-body" ref={terminalRef}>
{allOutput.map((entry, i) => {
const state =
entry.stepIdx === stepIndex
? "active"
: entry.stepIdx < stepIndex
? "past"
: "future";
return (
<div key={i} className="runner-terminal-line" data-step-state={state}>
{entry.line || " "}
</div>
);
})}
</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { highlight, type HighlightedCode } from "codehike/code";
import { BloomFilterDemoClient } from "./BloomFilterDemoClient";

const SNIPPETS: string[] = [
`const bits = new Uint8Array(16);
// all zeros, nothing added`,

`function add(item) {
for (const i of hashes(item)) {
bits[i] = 1;
}
}

add("alice"); // hashes -> [2, 7, 11]`,

`add("bob"); // hashes -> [4, 7, 13]
// bit 7 was already 1 from alice
// no way to tell who set it`,

`function check(item) {
return hashes(item).every(
(i) => bits[i] === 1,
);
}

check("alice"); // [2, 7, 11] all 1
// probably present, recheck`,

`check("carol"); // [0, 5, 9]
// bits[0] is 0
// definitely not present`,

`check("dave"); // [4, 11, 13]
// all three happen to be 1
// from alice + bob
// dave was never added
// false positive`,
];

export async function BloomFilterDemo() {
const highlighted = (await Promise.all(
SNIPPETS.map((value) => highlight({ value, lang: "typescript", meta: "" }, "github-from-css")),
)) as HighlightedCode[];
return <BloomFilterDemoClient snippets={highlighted} />;
}
Loading
Loading