use broadsheet::prelude::*;
const SPAWN: Vec2 = Vec2::new(280.0, 165.0);
const L0A: Vec2 = Vec2::new(545.0, 405.0);
const L0B: Vec2 = Vec2::new(735.0, 405.0);
const L1: Vec2 = Vec2::new(640.0, 480.0);
const L2: Vec2 = Vec2::new(640.0, 565.0);
fn node(k: u32) -> String {
format!("k{k}")
}
fn edge(k: u32) -> String {
format!("e{k}")
}
fn put(m: &mut Movie, k: u32, slot: Vec2, parent_slot: Option<Vec2>) {
m.play(act().set_text("inlab", &format!("put({k})")).dur(0.4));
m.play(par![
act().fade_in(&node(k)).dur(0.25),
act().move_to(&node(k), slot).dur(0.7).ease(InOutCubic),
]);
if let Some(_p) = parent_slot {
m.play(par![
act().fade_in(&edge(k)).dur(0.1),
act().grow_to(&edge(k), slot).dur(0.4).ease(OutCubic),
]);
}
m.play(act().pulse(&node(k)).dur(0.4));
m.wait(2.4);
}
fn main() {
let mut m = Movie::new("The LSM Tree", 1280, 720);
let slots: [(u32, Vec2, Option<u32>); 5] = [
(17, v(640.0, 165.0), None),
(8, v(555.0, 225.0), Some(17)),
(25, v(725.0, 225.0), Some(17)),
(3, v(490.0, 285.0), Some(8)),
(12, v(600.0, 285.0), Some(8)),
];
let slot_of = |k: u32| slots.iter().find(|s| s.0 == k).unwrap().1;
{
let mut s = m.scene();
s.rect("membox", v(640.0, 225.0), 440.0, 210.0)
.outlined()
.outline_color(INK)
.stroke(2.5)
.hidden();
s.text("memlabel", v(640.0, 105.0), "MEMTABLE — RAM")
.size(16.0)
.color(FADED)
.hidden();
s.line("divider", v(60.0, 345.0), v(1220.0, 345.0))
.color(FADED)
.stroke(1.5)
.hidden();
s.text("ramlab", v(110.0, 328.0), "MEMORY")
.size(13.0)
.color(FADED)
.hidden();
s.text("disklab", v(100.0, 364.0), "DISK")
.size(13.0)
.color(FADED)
.hidden();
for (k, _, parent) in slots.iter() {
if let Some(p) = parent {
let pslot = slots.iter().find(|s| s.0 == *p).unwrap().1;
s.arrow(&edge(*k), pslot, pslot)
.color(INK)
.stroke(2.0)
.z(1)
.hidden();
}
}
for (k, _, _) in slots.iter() {
s.circle(&node(*k), SPAWN, 26.0)
.stroke(2.5)
.z(5)
.hidden()
.label(&k.to_string());
}
s.text("inlab", v(170.0, 165.0), "")
.mono_bold()
.size(22.0)
.color(ACCENT)
.hidden();
s.polygon(
"pyramid",
vec![v(640.0, 383.0), v(990.0, 590.0), v(290.0, 590.0)],
)
.outlined()
.outline_color(FADED)
.stroke(1.5)
.z(0)
.hidden();
s.rect("run0a", L0A, 180.0, 36.0)
.outline_color(INK)
.stroke(2.0)
.z(3)
.hidden();
s.text("run0a.tag", v(0.0, 0.0), "3 · 8 · 12 · 17 · 25")
.size(13.0)
.z(4)
.follow("run0a", v(0.0, 0.0));
s.rect("run0b", L0B, 180.0, 36.0)
.outline_color(INK)
.stroke(2.0)
.z(3)
.hidden();
s.text("run0b.tag", v(0.0, 0.0), "5 · 9 · 21 · 40")
.size(13.0)
.z(4)
.follow("run0b", v(0.0, 0.0));
s.rect("run1", L1, 380.0, 40.0)
.outline_color(INK)
.stroke(2.0)
.z(3)
.hidden();
s.text("run1.tag", v(0.0, 0.0), "3 … 40 (merged, sorted)")
.size(13.0)
.z(4)
.follow("run1", v(0.0, 0.0));
s.rect("run2", L2, 700.0, 44.0)
.outline_color(INK)
.stroke(2.0)
.z(3)
.hidden();
s.text("run2.tag", v(0.0, 0.0), "0 … 99 (old, big, cold)")
.size(13.0)
.z(4)
.follow("run2", v(0.0, 0.0));
for (id, pos) in [("l0lab", L0A), ("l1lab", L1), ("l2lab", L2)] {
s.text(id, v(240.0, pos.y), &id[..2].to_uppercase())
.size(15.0)
.color(FADED)
.hidden();
}
s.text("getlab", v(1080.0, 165.0), "get(12)")
.mono_bold()
.size(24.0)
.color(BLUE)
.hidden();
s.text("found", v(1010.0, 480.0), "FOUND")
.serif()
.size(34.0)
.color(ACCENT)
.rot(-6.0)
.z(50)
.hidden();
s.text("caption", v(640.0, 650.0), "")
.size(20.0)
.color(FADED)
.wrap(1060.0)
.hidden();
}
m.wait(3.0);
m.play(act().set_text(
"caption",
"a write-optimized storage engine: writes never touch disk directly",
));
m.wait(5.0);
m.section("The Memtable");
m.play(par![
act().fade_in("membox").dur(0.6),
act().fade_in("memlabel").dur(0.6),
act().fade_in("divider").dur(0.6),
act().fade_in("ramlab").dur(0.6),
act().fade_in("disklab").dur(0.6),
]);
m.play(act().set_text(
"caption",
"every put() lands in an in-memory sorted structure — O(log n), no disk I/O",
));
m.play(act().fade_in("inlab").dur(0.2));
m.wait(0.8);
for (k, slot, parent) in slots.iter() {
put(&mut m, *k, *slot, parent.map(slot_of));
}
m.play(act().set_text(
"caption",
"sorted in memory, ready to be written out in one sequential pass",
));
m.wait(6.0);
m.section("Flush");
m.play(act().set_text(
"caption",
"memtable full → freeze it and flush: one immutable sorted run (SSTable)",
));
m.play(act().fade_out("inlab").dur(0.3));
m.play(act().fade_in("l0lab").dur(0.4));
{
let mut merge: Vec<Clip> = Vec::new();
for (k, _, parent) in slots.iter() {
merge.push(
act()
.move_to(&node(*k), L0A)
.dur(0.8)
.ease(InOutCubic)
.into(),
);
merge.push(act().fade_out(&node(*k)).dur(0.8).into());
if parent.is_some() {
merge.push(act().fade_out(&edge(*k)).dur(0.4).into());
}
}
m.play(Clip::par(merge));
}
m.play(par![
act().fade_in("run0a").dur(0.5),
act().pulse("run0a").dur(0.5)
]);
m.wait(4.5);
m.play(act().set_text(
"caption",
"writes keep coming — the next memtable flushes beside it",
));
m.play(par![
act().fade_in("run0b").dur(0.5),
act().pulse("run0b").dur(0.5)
]);
m.wait(6.5);
m.section("Compaction");
m.play(act().set_text(
"caption",
"L0 runs overlap — merge-sort them downward into one bigger run",
));
m.play(par![
act().highlight("run0a", ACCENT).dur(0.8),
act().highlight("run0b", ACCENT).dur(0.8),
]);
m.play(act().fade_in("l1lab").dur(0.4));
m.play(par![
act().move_to("run0a", L1).dur(0.9).ease(InOutCubic),
act().move_to("run0b", L1).dur(0.9).ease(InOutCubic),
act().fade_out("run0a").dur(0.9),
act().fade_out("run0b").dur(0.9),
]);
m.play(par![
act().fade_in("run1").dur(0.5),
act().pulse("run1").dur(0.5)
]);
m.wait(5.0);
m.play(act().set_text(
"caption",
"older data lives lower: bigger, colder, rarely rewritten",
));
m.play(par![
act().fade_in("l2lab").dur(0.4),
act().fade_in("run2").dur(0.6)
]);
m.wait(4.0);
m.play(act().fade_in("pyramid").dur(0.8));
m.play(act().set_text(
"caption",
"the shape is the point: small & fresh on top, wide & old below — a pyramid of sorted runs",
));
m.wait(6.5);
m.play(act().move_to("run0a", L0A).dur(0.01));
m.play(par![
act().fade_in("run0a").dur(0.5),
act().set_text("run0a.tag", "7 · 33 · 48").dur(0.4),
]);
m.play(act().set_text("caption", "meanwhile, new flushes keep arriving on top"));
m.wait(3.0);
m.section("The Read Path");
m.play(act().set_text(
"caption",
"get(12): check newest data first, walk down until found",
));
m.play(act().fade_in("getlab").dur(0.4));
m.wait(1.5);
m.play(seq![
act().highlight("membox", BLUE).dur(0.9),
wait(0.8),
act().highlight("run0a", BLUE).dur(0.9),
wait(0.8),
]);
m.play(act().set_text(
"caption",
"not in the memtable, not in L0 … (bloom filters skip most of these probes)",
));
m.wait(2.5);
m.play(seq![
act().highlight("run1", BLUE).dur(0.9),
par![
act().color_to("run1", ACCENT).dur(0.4),
act().pulse("run1").dur(0.6),
],
]);
m.play(par![
act().fade_in("found").dur(0.3),
act().pulse("found").dur(0.6)
]);
m.play(act().set_text(
"caption",
"found in L1 — worst case one probe per level, so keep the pyramid shallow",
));
m.wait(8.0);
m.play(par![
act().fade_out("getlab").dur(0.5),
act().fade_out("found").dur(0.5),
act().color_to("run1", PAPER).dur(0.5),
]);
m.play(act().set_text(
"caption",
"sequential writes, merge-sorted reads — the engine inside RocksDB, Cassandra & Kafka's cousin, the log",
));
m.wait(9.0);
broadsheet::run(m);
}