- Replace v0.3 content throughout; bump version label to v0.8 - New SVG: Krebs-cycle scheduler loop (6 stations, actor handoff as energy exchange, finalize leaving the cycle) - New SVG: gen_server dual-plane (model vs implementation mapping) - New SVG: slot state machine with packed word, park-epoch and all transitions (loom theorems annotated) - Updated SVG: module map (22 modules, 4 layers incl. slot_state, run_queue, raw_mutex, sync_shim, monitor, link, registry, trace) - Updated SVG: dep graph (runtime hub, OTP layer on public surface) - New sections: Slot State & Park-Epoch, Run Queue variants, GenServer, OTP (monitors/links/supervisors/registry) - Updated: init (slab allocation, counter-based termination), spawn (closure in slot AtomicPtr, stack pool, live_actors ordering), preemption (stop sentinel + StopSentinel/Outcome::Stopped), IO (epoch-matched completions, fd-leak fix), gotchas (lost-wakeup and global-mutex flipped to green; 4 new cards) - Yield-sources table grown to 9 rows (select, recv_timeout, call, request_stop) - Fix: swap local css2.css font ref for Google Fonts CDN
1736 lines
106 KiB
HTML
1736 lines
106 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en"><head>
|
||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>smarm — Deep Dive</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com/">
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=DM+Serif+Display:ital@0;1&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg: #0d0f14;
|
||
--surface: #13161e;
|
||
--surface2: #1a1e2a;
|
||
--border: #252a38;
|
||
--accent: #5b8af5;
|
||
--accent2: #f5a623;
|
||
--accent3: #4ecdc4;
|
||
--accent4: #ff6b6b;
|
||
--accent5: #a8e6cf;
|
||
--text: #c8d0e0;
|
||
--text-dim: #606880;
|
||
--text-bright: #e8eaf6;
|
||
--code-bg: #0a0c12;
|
||
--green: #56d364;
|
||
--yellow: #f0b429;
|
||
--red: #f85149;
|
||
--purple: #bc8cff;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
html { scroll-behavior: smooth; }
|
||
|
||
body {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: 'DM Sans', sans-serif;
|
||
font-size: 15px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
/* NAV */
|
||
nav {
|
||
position: fixed;
|
||
top: 0; left: 0; right: 0;
|
||
z-index: 100;
|
||
background: rgba(13,15,20,0.92);
|
||
backdrop-filter: blur(12px);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 0 2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2rem;
|
||
height: 52px;
|
||
}
|
||
|
||
.nav-brand {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-weight: 700;
|
||
font-size: 1rem;
|
||
color: var(--accent);
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
nav a {
|
||
text-decoration: none;
|
||
color: var(--text-dim);
|
||
font-size: 0.8rem;
|
||
font-weight: 500;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
nav a:hover { color: var(--text-bright); }
|
||
|
||
/* LAYOUT */
|
||
main {
|
||
max-width: 1100px;
|
||
margin: 0 auto;
|
||
padding: 80px 2rem 6rem;
|
||
}
|
||
|
||
section {
|
||
margin-bottom: 5rem;
|
||
}
|
||
|
||
/* TYPOGRAPHY */
|
||
.section-label {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.65rem;
|
||
letter-spacing: 0.2em;
|
||
text-transform: uppercase;
|
||
color: var(--accent);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
h1 {
|
||
font-family: 'DM Serif Display', serif;
|
||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||
color: var(--text-bright);
|
||
line-height: 1.1;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
h2 {
|
||
font-family: 'DM Serif Display', serif;
|
||
font-size: 2rem;
|
||
color: var(--text-bright);
|
||
line-height: 1.2;
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
h3 {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
color: var(--accent2);
|
||
margin-bottom: 0.8rem;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
p {
|
||
color: var(--text);
|
||
margin-bottom: 1rem;
|
||
max-width: 72ch;
|
||
}
|
||
|
||
p strong { color: var(--text-bright); font-weight: 500; }
|
||
|
||
code {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.82em;
|
||
background: var(--code-bg);
|
||
color: var(--accent3);
|
||
padding: 0.1em 0.35em;
|
||
border-radius: 3px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
pre {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.8rem;
|
||
background: var(--code-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 1.2rem 1.4rem;
|
||
overflow-x: auto;
|
||
line-height: 1.6;
|
||
color: var(--text);
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
pre .kw { color: var(--purple); }
|
||
pre .fn { color: var(--accent); }
|
||
pre .ty { color: var(--accent3); }
|
||
pre .st { color: var(--accent5); }
|
||
pre .cm { color: var(--text-dim); font-style: italic; }
|
||
pre .nu { color: var(--accent2); }
|
||
pre .mc { color: var(--accent4); }
|
||
|
||
/* HERO */
|
||
.hero {
|
||
padding: 4rem 0 2rem;
|
||
}
|
||
|
||
.hero-tagline {
|
||
font-family: 'DM Serif Display', serif;
|
||
font-style: italic;
|
||
font-size: 1.2rem;
|
||
color: var(--text-dim);
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.pitch-row {
|
||
display: flex;
|
||
gap: 1.5rem;
|
||
margin-top: 2rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.pitch-card {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 1.2rem 1.4rem;
|
||
}
|
||
|
||
.pitch-card .label {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.65rem;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
color: var(--text-dim);
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
.pitch-card p {
|
||
font-size: 0.9rem;
|
||
color: var(--text);
|
||
margin: 0;
|
||
}
|
||
|
||
/* SVG DIAGRAMS */
|
||
.diagram-wrap {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 2rem;
|
||
overflow-x: auto;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.diagram-wrap svg {
|
||
display: block;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* SEQUENCE / FLOW */
|
||
.flow-diagram {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
max-width: 800px;
|
||
}
|
||
|
||
.flow-step {
|
||
display: flex;
|
||
gap: 1rem;
|
||
position: relative;
|
||
}
|
||
|
||
.flow-step::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 19px;
|
||
top: 38px;
|
||
bottom: -2px;
|
||
width: 2px;
|
||
background: var(--border);
|
||
}
|
||
|
||
.flow-step:last-child::before { display: none; }
|
||
|
||
.flow-num {
|
||
width: 40px;
|
||
height: 40px;
|
||
flex-shrink: 0;
|
||
background: var(--surface2);
|
||
border: 1.5px solid var(--accent);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
margin-top: 0.5rem;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.flow-body {
|
||
padding: 0.5rem 0 1.6rem;
|
||
flex: 1;
|
||
}
|
||
|
||
.flow-body h4 {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.82rem;
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.flow-body p {
|
||
font-size: 0.88rem;
|
||
color: var(--text-dim);
|
||
margin: 0;
|
||
}
|
||
|
||
.flow-body .tag {
|
||
display: inline-block;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.65rem;
|
||
background: var(--code-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: 0.1em 0.4em;
|
||
color: var(--accent3);
|
||
margin-right: 0.3rem;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* MODULE TABLE */
|
||
.module-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.module-grid { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
.module-card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 1rem 1.2rem;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.module-card:hover {
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.module-name {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.85rem;
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
margin-bottom: 0.2rem;
|
||
}
|
||
|
||
.module-layer {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.6rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.1em;
|
||
color: var(--text-dim);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.module-card p {
|
||
font-size: 0.82rem;
|
||
color: var(--text-dim);
|
||
margin: 0;
|
||
}
|
||
|
||
/* DIVIDER */
|
||
.divider {
|
||
height: 1px;
|
||
background: linear-gradient(to right, transparent, var(--border), transparent);
|
||
margin: 4rem 0;
|
||
}
|
||
|
||
/* THREAD STATE TABLE */
|
||
.state-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.78rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.state-table th {
|
||
background: var(--surface2);
|
||
color: var(--text-dim);
|
||
padding: 0.6rem 1rem;
|
||
text-align: left;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
font-size: 0.65rem;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.state-table td {
|
||
padding: 0.6rem 1rem;
|
||
border-bottom: 1px solid var(--border);
|
||
color: var(--text);
|
||
}
|
||
|
||
.state-table tr:last-child td { border-bottom: none; }
|
||
.state-table tr:hover td { background: var(--surface2); }
|
||
|
||
.pill {
|
||
display: inline-block;
|
||
padding: 0.15em 0.5em;
|
||
border-radius: 20px;
|
||
font-size: 0.7em;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.pill-green { background: rgba(86,211,100,0.12); color: var(--green); }
|
||
.pill-yellow { background: rgba(240,180,41,0.12); color: var(--yellow); }
|
||
.pill-red { background: rgba(248,81,73,0.12); color: var(--red); }
|
||
.pill-blue { background: rgba(91,138,245,0.12); color: var(--accent); }
|
||
|
||
/* CALLOUT */
|
||
.callout {
|
||
display: flex;
|
||
gap: 1rem;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-left: 3px solid var(--accent2);
|
||
border-radius: 0 8px 8px 0;
|
||
padding: 1rem 1.2rem;
|
||
margin-bottom: 1.5rem;
|
||
max-width: 72ch;
|
||
}
|
||
|
||
.callout-icon {
|
||
font-size: 1.1rem;
|
||
flex-shrink: 0;
|
||
margin-top: 0.1rem;
|
||
}
|
||
|
||
.callout p {
|
||
font-size: 0.88rem;
|
||
margin: 0;
|
||
color: var(--text);
|
||
}
|
||
|
||
/* COLUMNS */
|
||
.two-col {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 2rem;
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.two-col { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
/* TICK */
|
||
.warn { color: var(--yellow); }
|
||
.good { color: var(--green); }
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav>
|
||
<span class="nav-brand">smarm v0.8</span>
|
||
<a href="#overview">Overview</a>
|
||
<a href="#modules">Modules</a>
|
||
<a href="#deps">Deps</a>
|
||
<a href="#init">Init</a>
|
||
<a href="#cycle">Cycle</a>
|
||
<a href="#slot-state">Slots</a>
|
||
<a href="#run-queue">Queue</a>
|
||
<a href="#spawn">Spawn</a>
|
||
<a href="#preempt">Preempt</a>
|
||
<a href="#io">IO</a>
|
||
<a href="#genserver">GenServer</a>
|
||
<a href="#otp">OTP</a>
|
||
<a href="#gotchas">Gotchas</a>
|
||
</nav>
|
||
|
||
<main>
|
||
|
||
<!-- HERO -->
|
||
<section class="hero" id="overview">
|
||
<div class="section-label">smarm — Stubborn Marks Actor Runtime Machinery</div>
|
||
<h1>Green-Thread Actor Runtime</h1>
|
||
<p class="hero-tagline">Erlang's isolation model. Rust's zero-copy ownership. No function colouring.</p>
|
||
<p>
|
||
smarm is a concurrent runtime for Rust. Each <strong>actor</strong> is a green thread with its own
|
||
<code>mmap</code>'d stack. N OS threads share a run queue backed by a <strong>fixed slot slab</strong>
|
||
(16,384 slots, ~4 MiB, allocated once) with lock-free slot lookup. Actors communicate
|
||
exclusively via <strong>message passing</strong> (owned values over channels); no shared mutable state
|
||
without an explicit <code>Arc<Mutex<T>></code>.
|
||
</p>
|
||
<p>
|
||
Preemption is <strong>allocator-driven</strong>: every Nth heap allocation, smarm reads RDTSC and yields
|
||
the actor if its timeslice has expired. No OS signals, no separate timer thread for scheduling.
|
||
Since v0.3 the monolithic <code>Mutex<SharedState></code> is gone: scheduling state lives in one
|
||
atomic word per slot, the loom-checked transitions of which close every lost-wakeup window by
|
||
construction. On top sits an OTP layer: <code>gen_server</code>, supervisors with restart strategies,
|
||
monitors, links, and a named-pid registry.
|
||
</p>
|
||
|
||
<div class="pitch-row">
|
||
<div class="pitch-card">
|
||
<div class="label">vs async/await</div>
|
||
<p>No function colouring. No <code>Box<dyn Future></code>. No poll state machines. Just plain Rust functions that block.</p>
|
||
</div>
|
||
<div class="pitch-card">
|
||
<div class="label">vs OS threads</div>
|
||
<p>64 KB stacks instead of 8 MB. Context switch in ~10–20 ns (6 GPR saves + ret) instead of kernel mode.</p>
|
||
</div>
|
||
<div class="pitch-card">
|
||
<div class="label">vs Erlang BEAM</div>
|
||
<p>Zero-copy ownership via Rust's type system. No GC pause. Message passing is a <code>move</code>, not a clone — with gen_server, supervision, links and monitors all the same.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- MODULE MAP -->
|
||
<section id="modules">
|
||
<div class="section-label">Architecture</div>
|
||
<h2>Module Map</h2>
|
||
<p>22 source modules, four layers. Layer 0 has no smarm dependencies. Layer 1 is new in the
|
||
v0.5–v0.8 line and is the heart of the runtime: two small modules whose every transition is
|
||
model-checked with <strong>loom</strong>. Layer 2 is the machinery, with <code>runtime.rs</code> as the hub.
|
||
Layer 3 is the public surface plus the OTP layer built on it.</p>
|
||
|
||
<div class="diagram-wrap">
|
||
<svg width="920" height="560" viewBox="0 0 920 560" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;font-family:'JetBrains Mono',monospace">
|
||
<defs>
|
||
<marker id="mm-arr" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#3a4060"/></marker>
|
||
<marker id="mm-acc" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#5b8af5"/></marker>
|
||
</defs>
|
||
|
||
<!-- LAYER 0 -->
|
||
<text x="30" y="50" fill="#606880" font-size="9" letter-spacing="2">LAYER 0 — PRIMITIVES (no smarm deps)</text>
|
||
<g>
|
||
<rect x="30" y="62" width="110" height="44" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="85" y="80" fill="#5b8af5" font-size="10" font-weight="700" text-anchor="middle">stack</text>
|
||
<text x="85" y="96" fill="#606880" font-size="8" text-anchor="middle">mmap + guard</text>
|
||
<rect x="152" y="62" width="110" height="44" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="207" y="80" fill="#5b8af5" font-size="10" font-weight="700" text-anchor="middle">context</text>
|
||
<text x="207" y="96" fill="#606880" font-size="8" text-anchor="middle">naked asm CSW</text>
|
||
<rect x="274" y="62" width="110" height="44" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="329" y="80" fill="#5b8af5" font-size="10" font-weight="700" text-anchor="middle">preempt</text>
|
||
<text x="329" y="96" fill="#606880" font-size="8" text-anchor="middle">alloc hook + stop</text>
|
||
<rect x="396" y="62" width="110" height="44" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="451" y="80" fill="#5b8af5" font-size="10" font-weight="700" text-anchor="middle">pid</text>
|
||
<text x="451" y="96" fill="#606880" font-size="8" text-anchor="middle">(index, gen)</text>
|
||
<rect x="518" y="62" width="110" height="44" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="573" y="80" fill="#5b8af5" font-size="10" font-weight="700" text-anchor="middle">timer</text>
|
||
<text x="573" y="96" fill="#606880" font-size="8" text-anchor="middle">min-heap + epoch</text>
|
||
<rect x="640" y="62" width="110" height="44" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="695" y="80" fill="#5b8af5" font-size="10" font-weight="700" text-anchor="middle">raw_mutex</text>
|
||
<text x="695" y="96" fill="#606880" font-size="8" text-anchor="middle">futex, no poison</text>
|
||
<rect x="762" y="62" width="110" height="44" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="817" y="80" fill="#5b8af5" font-size="10" font-weight="700" text-anchor="middle">sync_shim</text>
|
||
<text x="817" y="96" fill="#606880" font-size="8" text-anchor="middle">std / loom</text>
|
||
</g>
|
||
|
||
<!-- LAYER 1 -->
|
||
<text x="30" y="160" fill="#606880" font-size="9" letter-spacing="2">LAYER 1 — THE LOOM-CHECKED PROTOCOL CORE</text>
|
||
<rect x="240" y="172" width="200" height="48" rx="6" fill="#0d1220" stroke="#bc8cff" stroke-width="1.5"/>
|
||
<text x="340" y="191" fill="#bc8cff" font-size="10" font-weight="700" text-anchor="middle">slot_state</text>
|
||
<text x="340" y="207" fill="#606880" font-size="8" text-anchor="middle">(gen|epoch|state) word, all CAS</text>
|
||
<rect x="480" y="172" width="200" height="48" rx="6" fill="#0d1220" stroke="#bc8cff" stroke-width="1.5"/>
|
||
<text x="580" y="191" fill="#bc8cff" font-size="10" font-weight="700" text-anchor="middle">run_queue</text>
|
||
<text x="580" y="207" fill="#606880" font-size="8" text-anchor="middle">rq-mutex | rq-mpmc | rq-striped</text>
|
||
|
||
<!-- LAYER 2 -->
|
||
<text x="30" y="272" fill="#606880" font-size="9" letter-spacing="2">LAYER 2 — RUNTIME MACHINERY</text>
|
||
<g>
|
||
<rect x="30" y="284" width="100" height="44" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="80" y="302" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">actor</text>
|
||
<text x="80" y="318" fill="#606880" font-size="8" text-anchor="middle">trampoline</text>
|
||
<rect x="142" y="284" width="100" height="44" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="192" y="302" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">channel</text>
|
||
<text x="192" y="318" fill="#606880" font-size="8" text-anchor="middle">MPSC + select</text>
|
||
<rect x="254" y="284" width="100" height="44" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="304" y="302" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">mutex</text>
|
||
<text x="304" y="318" fill="#606880" font-size="8" text-anchor="middle">timeout, FIFO</text>
|
||
<rect x="366" y="284" width="100" height="44" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="416" y="302" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">io</text>
|
||
<text x="416" y="318" fill="#606880" font-size="8" text-anchor="middle">epoll + pool</text>
|
||
<rect x="478" y="284" width="100" height="44" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="528" y="302" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">monitor</text>
|
||
<text x="528" y="318" fill="#606880" font-size="8" text-anchor="middle">one-shot Down</text>
|
||
<rect x="590" y="284" width="100" height="44" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="640" y="302" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">link</text>
|
||
<text x="640" y="318" fill="#606880" font-size="8" text-anchor="middle">2-way + trap</text>
|
||
<rect x="702" y="284" width="100" height="44" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="752" y="302" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">registry</text>
|
||
<text x="752" y="318" fill="#606880" font-size="8" text-anchor="middle">name ↔ pid</text>
|
||
<rect x="814" y="284" width="80" height="44" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="854" y="302" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">trace</text>
|
||
<text x="854" y="318" fill="#606880" font-size="8" text-anchor="middle">perfetto</text>
|
||
</g>
|
||
<rect x="310" y="352" width="300" height="52" rx="6" fill="#0d1220" stroke="#5b8af5" stroke-width="2"/>
|
||
<text x="460" y="372" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">runtime.rs — the hub</text>
|
||
<text x="460" y="390" fill="#606880" font-size="8" text-anchor="middle">slot slab · spawn/finalize · schedule_loop · unpark</text>
|
||
|
||
<!-- LAYER 3 -->
|
||
<text x="30" y="466" fill="#606880" font-size="9" letter-spacing="2">LAYER 3 — PUBLIC API + OTP LAYER</text>
|
||
<g>
|
||
<rect x="120" y="478" width="160" height="48" rx="6" fill="#0a0c12" stroke="#f5a623" stroke-width="1.5"/>
|
||
<text x="200" y="497" fill="#f5a623" font-size="10" font-weight="700" text-anchor="middle">scheduler</text>
|
||
<text x="200" y="513" fill="#606880" font-size="8" text-anchor="middle">spawn · park · stop · join</text>
|
||
<rect x="300" y="478" width="160" height="48" rx="6" fill="#0a0c12" stroke="#f5a623" stroke-width="1.5"/>
|
||
<text x="380" y="497" fill="#f5a623" font-size="10" font-weight="700" text-anchor="middle">lib.rs</text>
|
||
<text x="380" y="513" fill="#606880" font-size="8" text-anchor="middle">re-exports + GlobalAlloc</text>
|
||
<rect x="480" y="478" width="160" height="48" rx="6" fill="#0a0c12" stroke="#f5a623" stroke-width="1.5"/>
|
||
<text x="560" y="497" fill="#f5a623" font-size="10" font-weight="700" text-anchor="middle">gen_server</text>
|
||
<text x="560" y="513" fill="#606880" font-size="8" text-anchor="middle">call / cast / info / down</text>
|
||
<rect x="660" y="478" width="160" height="48" rx="6" fill="#0a0c12" stroke="#f5a623" stroke-width="1.5"/>
|
||
<text x="740" y="497" fill="#f5a623" font-size="10" font-weight="700" text-anchor="middle">supervisor</text>
|
||
<text x="740" y="513" fill="#606880" font-size="8" text-anchor="middle">strategies + intensity</text>
|
||
</g>
|
||
|
||
<!-- inter-layer arrows -->
|
||
<line x1="340" y1="106" x2="340" y2="170" stroke="#3a4060" stroke-width="1" marker-end="url(#mm-arr)"/>
|
||
<line x1="580" y1="106" x2="580" y2="170" stroke="#3a4060" stroke-width="1" marker-end="url(#mm-arr)"/>
|
||
<path d="M 340 220 Q 360 238 360 268 L 360 330 Q 360 346 384 351" fill="none" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#mm-acc)"/>
|
||
<path d="M 580 220 Q 584 238 584 268 L 584 330 Q 584 346 562 351" fill="none" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#mm-acc)"/>
|
||
<line x1="200" y1="328" x2="350" y2="352" stroke="#3a4060" stroke-width="1" marker-end="url(#mm-arr)"/>
|
||
<line x1="700" y1="328" x2="560" y2="354" stroke="#3a4060" stroke-width="1" marker-end="url(#mm-arr)"/>
|
||
<line x1="380" y1="404" x2="280" y2="476" stroke="#5b8af5" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#mm-acc)"/>
|
||
<line x1="500" y1="404" x2="540" y2="476" stroke="#5b8af5" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#mm-acc)"/>
|
||
</svg>
|
||
</div>
|
||
|
||
<div class="module-grid">
|
||
<div class="module-card">
|
||
<div class="module-name">stack</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p><code>mmap</code> a contiguous region, <code>mprotect</code> the bottom page to <code>PROT_NONE</code> as a guard. Overflow → SIGSEGV. New in v0.8: a <strong>stack pool</strong> in <code>RuntimeInner</code> recycles stacks across spawns (cap: <code>threads × 4</code>), avoiding mmap/munmap churn.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">context</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p>Two <code>#[naked]</code> assembly functions (<code>switch_to_actor</code>, <code>switch_to_scheduler</code>). Save 6 callee-saved GPRs, swap <code>rsp</code>, restore, <code>ret</code>. Thread-locals hold each side's saved stack pointer. XMM registers intentionally not saved — the compiler spills them at Rust call sites.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">preempt</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p>Implements <code>GlobalAlloc</code>. Every Nth alloc (or <code>check!()</code>), read RDTSC; timeslice expired → <code>switch_to_scheduler()</code>. Also hosts <strong>cooperative cancellation</strong>: <code>check_cancelled()</code> reads the on-CPU actor's stop flag and raises the <code>StopSentinel</code> panic so the stack unwinds cleanly.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">pid</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p><code>Pid(u32 index, u32 generation)</code>. Index = slot in the slab; generation bumps on reclaim and is never reused. A stale <code>Pid</code> fails the generation check <em>atomically with</em> any transition it attempts — ABA is structurally impossible.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">timer</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p>Min-heap by deadline. <code>Reason::Sleep</code> and <code>Reason::WaitTimeout</code> entries now carry the wait's <strong>park-epoch</strong>: a stale entry (wait already satisfied) fails the epoch match and is a guaranteed no-op rather than a heuristic one.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">raw_mutex</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p>Three-state futex mutex (Drepper's mutex3) that <strong>cannot poison</strong>. The guard enters <code>NoPreempt</code>, which both bounds lock hold times and structurally prevents the stop sentinel from unwinding a critical section. Used for slot cold data, free list, stack pool, registry.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">sync_shim</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p>std vs <code>loom::sync</code> indirection for the two model-checked modules. Build the models with <code>RUSTFLAGS="--cfg loom" cargo test --lib --release</code>. Everything else uses std directly — context switches and futexes aren't loom-able.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--purple)">slot_state</div>
|
||
<div class="module-layer">Layer 1 · loom-checked core</div>
|
||
<p>The per-slot state machine as a standalone unit: one <code>AtomicU64</code> packing <code>(gen | park-epoch | state)</code>, every transition a CAS on the whole word. Loom proves lost-wakeup, double-enqueue, stale-epoch and ABA unreachable. See the <a href="#slot-state" style="color:var(--accent)">Slot State</a> section.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--purple)">run_queue</div>
|
||
<div class="module-layer">Layer 1 · loom-checked core</div>
|
||
<p>The global queue behind a compile-time choice: <code>rq-mutex</code> (default), <code>rq-mpmc</code> (Vyukov ring), <code>rq-striped</code> (M rings). All satisfy the same contract: push is infallible, a pid is in the queue at most once. See <a href="#run-queue" style="color:var(--accent)">Run Queue</a>.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">actor</div>
|
||
<div class="module-layer">Layer 2 · machinery</div>
|
||
<p>Owns the <code>Stack</code>; defines the <code>trampoline</code> every first <code>ret</code> lands in. Runs the closure inside <code>catch_unwind</code>; outcome is <code>Exit</code>, <code>Panic(payload)</code>, or — new — <code>Stopped</code> when the unwind was the <code>StopSentinel</code> from cooperative cancellation.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">runtime</div>
|
||
<div class="module-layer">Layer 2 · the hub</div>
|
||
<p>Owns <code>RuntimeInner</code>: the fixed slot slab, free list, run queue, timers, IO handle, <code>live_actors</code> counter, per-thread stats. Hosts <code>spawn</code>, <code>finalize_actor</code>, the unpark protocol, and <code>schedule_loop</code>. The old <code>Mutex<SharedState></code> no longer exists.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">channel</div>
|
||
<div class="module-layer">Layer 2 · machinery</div>
|
||
<p>Unbounded MPSC, plus — new — <code>recv_timeout</code>, <code>select</code>, and <code>select_timeout</code> over any set of <code>Selectable</code> arms with ready-index priority in declaration order. A closed arm counts as ready-forever, which gen_server leans on for shutdown.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">mutex</div>
|
||
<div class="module-layer">Layer 2 · machinery</div>
|
||
<p>Actor-aware mutex with mandatory timeout (default 30s). FIFO waiter queue, <code>WaitTimeout</code> timer entry, park. Timer and grant now race through the epoch-matched unpark — exactly one of them wakes the waiter, the other is a proven no-op.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">io</div>
|
||
<div class="module-layer">Layer 2 · machinery</div>
|
||
<p>Two background OS threads: epoll thread (EPOLLONESHOT, pushes <code>FdReady</code>) and pool thread (blocking closures in <code>catch_unwind</code>, pushes <code>Blocking</code>). Completions and waiter registrations now carry epochs; the v0.3 fd-leak-on-death gap is closed by an unwind guard plus a belt-and-braces <code>EPOLL_CTL_DEL</code>.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">monitor · link</div>
|
||
<div class="module-layer">Layer 2 · machinery</div>
|
||
<p><code>monitor(pid)</code>: unidirectional, one-shot <code>Down</code> on a private channel; <code>demonitor</code> takes it back; dead targets yield an immediate <code>NoProc</code>. <code>link(pid)</code>: bidirectional and persistent — abnormal death propagates as a transitive stop cascade, or as an <code>ExitSignal</code> message if the peer called <code>trap_exit</code>.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">registry</div>
|
||
<div class="module-layer">Layer 2 · machinery</div>
|
||
<p>Erlang-style name registry: a bimap (name ↔ pid) under one <code>RawMutex</code>, O(1) both directions. Cleanup is <strong>lazy</strong>: liveness is checked against the generation-checked slot word on contact, so dead bindings behave as absent — zero coupling to the actor lifecycle.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">trace</div>
|
||
<div class="module-layer">Layer 2 · machinery</div>
|
||
<p>Per-event tracing behind <code>--features smarm-trace</code>, zero cost without it. MPSC to a dedicated drain thread that batches to disk; output is Chrome-trace JSON viewable in Perfetto. <code>record()</code> disables preemption (it can allocate).</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">scheduler · lib</div>
|
||
<div class="module-layer">Layer 3 · public facade</div>
|
||
<p>The public surface: <code>spawn</code>, <code>spawn_under</code>, <code>yield_now</code>, <code>sleep</code>, <code>request_stop</code>, <code>join</code>, IO waits, <code>NoPreempt</code>. <code>lib.rs</code> re-exports everything and installs the <code>PreemptingAllocator</code> as <code>#[global_allocator]</code>.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">gen_server · supervisor</div>
|
||
<div class="module-layer">Layer 3 · OTP</div>
|
||
<p>Erlang's two workhorses, built on channels + select + spawn — not on runtime internals. <code>gen_server</code>: call/cast/info/down over one actor. <code>supervisor</code>: <code>OneForOne</code> / <code>OneForAll</code> / <code>RestForOne</code> strategies with a restart-intensity cap (default 3 per 5s).</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- DEPENDENCY GRAPH -->
|
||
<section id="deps">
|
||
<div class="section-label">Dependency Graph</div>
|
||
<h2>Who Imports What</h2>
|
||
<p>The shape survives the v0.8 rewrite: <code>runtime.rs</code> is the hub. What changed is what feeds it —
|
||
the scheduling state itself (<code>slot_state</code>) and the queue (<code>run_queue</code>) are now standalone,
|
||
loom-checked inputs rather than fields of a big struct. The OTP layer deliberately sits on the
|
||
<em>public</em> surface: <code>gen_server</code> and the supervisor use channels, <code>select</code>, monitors and
|
||
<code>spawn</code> the same way user code would.</p>
|
||
|
||
<div class="diagram-wrap">
|
||
<svg width="880" height="430" viewBox="0 0 880 430" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;font-family:'JetBrains Mono',monospace">
|
||
<defs>
|
||
<marker id="dg-blue" markerWidth="7" markerHeight="7" refX="5" refY="3" orient="auto"><path d="M0,0 L0,6 L7,3 z" fill="#5b8af5"/></marker>
|
||
<marker id="dg-orange" markerWidth="7" markerHeight="7" refX="5" refY="3" orient="auto"><path d="M0,0 L0,6 L7,3 z" fill="#f5a623"/></marker>
|
||
<marker id="dg-purple" markerWidth="7" markerHeight="7" refX="5" refY="3" orient="auto"><path d="M0,0 L0,6 L7,3 z" fill="#bc8cff"/></marker>
|
||
</defs>
|
||
|
||
<!-- top feeds -->
|
||
<g>
|
||
<rect x="27" y="24" width="98" height="42" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="76" y="41" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">stack</text>
|
||
<text x="76" y="56" fill="#606880" font-size="8" text-anchor="middle">+ stack pool</text>
|
||
<rect x="131" y="24" width="98" height="42" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="180" y="41" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">context</text>
|
||
<text x="180" y="56" fill="#606880" font-size="8" text-anchor="middle">switch fns</text>
|
||
<rect x="235" y="24" width="98" height="42" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="284" y="41" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">preempt</text>
|
||
<text x="284" y="56" fill="#606880" font-size="8" text-anchor="middle">stop flag</text>
|
||
<rect x="339" y="24" width="98" height="42" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="388" y="41" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">actor</text>
|
||
<text x="388" y="56" fill="#606880" font-size="8" text-anchor="middle">trampoline</text>
|
||
<rect x="443" y="24" width="98" height="42" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="492" y="41" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">timer</text>
|
||
<text x="492" y="56" fill="#606880" font-size="8" text-anchor="middle">min-heap</text>
|
||
<rect x="547" y="24" width="98" height="42" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="596" y="41" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">io</text>
|
||
<text x="596" y="56" fill="#606880" font-size="8" text-anchor="middle">epoll + pool</text>
|
||
<rect x="651" y="24" width="98" height="42" rx="5" fill="#0d1220" stroke="#bc8cff" stroke-width="1.5"/>
|
||
<text x="700" y="41" fill="#bc8cff" font-size="10" font-weight="700" text-anchor="middle">slot_state</text>
|
||
<text x="700" y="56" fill="#606880" font-size="8" text-anchor="middle">the word</text>
|
||
<rect x="755" y="24" width="98" height="42" rx="5" fill="#0d1220" stroke="#bc8cff" stroke-width="1.5"/>
|
||
<text x="804" y="41" fill="#bc8cff" font-size="10" font-weight="700" text-anchor="middle">run_queue</text>
|
||
<text x="804" y="56" fill="#606880" font-size="8" text-anchor="middle">3 variants</text>
|
||
</g>
|
||
|
||
<!-- hub -->
|
||
<rect x="350" y="170" width="200" height="56" rx="8" fill="#0d1220" stroke="#5b8af5" stroke-width="2"/>
|
||
<text x="450" y="192" fill="#5b8af5" font-size="12" font-weight="700" text-anchor="middle">runtime.rs</text>
|
||
<text x="450" y="210" fill="#606880" font-size="9" text-anchor="middle">slot slab · schedule_loop</text>
|
||
|
||
<!-- feed arrows -->
|
||
<line x1="76" y1="66" x2="368" y2="170" stroke="#5b8af5" stroke-width="1" marker-end="url(#dg-blue)"/>
|
||
<line x1="180" y1="66" x2="388" y2="170" stroke="#5b8af5" stroke-width="1" marker-end="url(#dg-blue)"/>
|
||
<line x1="284" y1="66" x2="410" y2="170" stroke="#5b8af5" stroke-width="1" marker-end="url(#dg-blue)"/>
|
||
<line x1="388" y1="66" x2="432" y2="170" stroke="#5b8af5" stroke-width="1" marker-end="url(#dg-blue)"/>
|
||
<line x1="492" y1="66" x2="464" y2="170" stroke="#5b8af5" stroke-width="1" marker-end="url(#dg-blue)"/>
|
||
<line x1="596" y1="66" x2="490" y2="170" stroke="#5b8af5" stroke-width="1" marker-end="url(#dg-blue)"/>
|
||
<line x1="700" y1="66" x2="516" y2="170" stroke="#bc8cff" stroke-width="1.5" marker-end="url(#dg-purple)"/>
|
||
<line x1="804" y1="66" x2="540" y2="170" stroke="#bc8cff" stroke-width="1.5" marker-end="url(#dg-purple)"/>
|
||
|
||
<!-- bottom-left: callers -->
|
||
<rect x="40" y="320" width="100" height="42" rx="5" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="90" y="337" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">channel</text>
|
||
<text x="90" y="352" fill="#606880" font-size="8" text-anchor="middle">+ select</text>
|
||
<rect x="152" y="320" width="100" height="42" rx="5" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="202" y="337" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">mutex</text>
|
||
<text x="202" y="352" fill="#606880" font-size="8" text-anchor="middle">lock_timeout</text>
|
||
<rect x="264" y="320" width="100" height="42" rx="5" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="314" y="337" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">registry</text>
|
||
<text x="314" y="352" fill="#606880" font-size="8" text-anchor="middle">name ↔ pid</text>
|
||
<line x1="100" y1="318" x2="360" y2="222" stroke="#5b8af5" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#dg-blue)"/>
|
||
<line x1="212" y1="318" x2="378" y2="226" stroke="#5b8af5" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#dg-blue)"/>
|
||
<line x1="324" y1="318" x2="396" y2="228" stroke="#5b8af5" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#dg-blue)"/>
|
||
<text x="202" y="382" fill="#606880" font-size="8" text-anchor="middle">begin_wait · park · unpark_at</text>
|
||
<text x="202" y="394" fill="#606880" font-size="8" text-anchor="middle">(through scheduler's facade)</text>
|
||
|
||
<!-- bottom-center: facade -->
|
||
<rect x="420" y="320" width="220" height="48" rx="8" fill="#0a0c12" stroke="#f5a623" stroke-width="2"/>
|
||
<text x="530" y="339" fill="#f5a623" font-size="11" font-weight="700" text-anchor="middle">scheduler.rs / lib.rs</text>
|
||
<text x="530" y="355" fill="#606880" font-size="8" text-anchor="middle">public surface · GlobalAlloc</text>
|
||
<line x1="470" y1="226" x2="510" y2="318" stroke="#f5a623" stroke-width="1.5" stroke-dasharray="5,2" marker-end="url(#dg-orange)"/>
|
||
|
||
<!-- bottom-right: OTP -->
|
||
<rect x="680" y="300" width="180" height="88" rx="8" fill="#13161e" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="770" y="320" fill="#a8e6cf" font-size="10" font-weight="700" text-anchor="middle">OTP layer</text>
|
||
<text x="770" y="336" fill="#606880" font-size="8" text-anchor="middle">gen_server · supervisor</text>
|
||
<text x="770" y="349" fill="#606880" font-size="8" text-anchor="middle">monitor · link</text>
|
||
<text x="770" y="364" fill="#606880" font-size="8" text-anchor="middle">built on channels, select,</text>
|
||
<text x="770" y="377" fill="#606880" font-size="8" text-anchor="middle">spawn — not on internals</text>
|
||
<line x1="678" y1="344" x2="642" y2="344" stroke="#f5a623" stroke-width="1.2" marker-end="url(#dg-orange)"/>
|
||
</svg>
|
||
</div>
|
||
|
||
<div class="callout">
|
||
<span class="callout-icon">⚠</span>
|
||
<p><strong>Circular dependency, still intentional:</strong> <code>channel</code> and <code>mutex</code> call <code>scheduler::unpark_at()</code>, which calls into <code>runtime</code> — and <code>runtime</code>'s <code>schedule_loop</code> resumes actors that run channel/mutex code. It works because an unpark is now a single CAS on the slot word plus an enqueue: it never blocks, and preemption is disabled while any smarm-internal lock (<code>RawMutex</code> guard, queue mutex) is held.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- INIT SEQUENCE -->
|
||
<section id="init">
|
||
<div class="section-label">Initialisation</div>
|
||
<h2>What Happens When You Call <code>run(f)</code></h2>
|
||
<p>Starting from user code calling <code>smarm::run(|| { ... })</code>. The single-threaded <code>run()</code> is a wrapper around <code>runtime::init(Config::exact(1)).run(f)</code>.</p>
|
||
|
||
<div class="flow-diagram">
|
||
<div class="flow-step">
|
||
<div class="flow-num">1</div>
|
||
<div class="flow-body">
|
||
<h4>Install panic hook (once)</h4>
|
||
<p>A <code>OnceLock</code> guard installs a custom panic hook that suppresses output inside actor context. Without this, concurrent actor panics can deadlock Rust's default backtrace printer (non-reentrant internal lock). The previous hook is chained for panics outside actors.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">2</div>
|
||
<div class="flow-body">
|
||
<h4>Allocate the slot slab <span class="tag">runtime.rs</span></h4>
|
||
<p><code>Box<[Slot]></code> with <code>max_actors</code> entries (default 16,384, ~4 MiB) is allocated <strong>once</strong> and never moves — that's what makes lock-free slot lookup sound. Every index goes onto the free list. Exhausting the slab is a loud panic naming the <code>Config::max_actors</code> knob.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">3</div>
|
||
<div class="flow-body">
|
||
<h4>Start <code>IoThread</code> <span class="tag">io.rs</span></h4>
|
||
<p>Creates a wake pipe (<code>O_NONBLOCK</code>), an <code>epollfd</code>, and a shutdown pipe registered in the epollfd. Spawns the <strong>epoll thread</strong> (<code>epoll_wait</code> loop) and the <strong>pool thread</strong> (blocking-work mpsc receiver). Both share a completion <code>VecDeque</code> behind a mutex.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">4</div>
|
||
<div class="flow-body">
|
||
<h4>Install <code>RUNTIME</code> thread-local <span class="tag">runtime.rs</span></h4>
|
||
<p><code>Arc<RuntimeInner></code> is cloned into the calling thread's <code>RUNTIME</code> thread-local. This makes <code>with_runtime()</code> work on the calling thread immediately — needed for the next step.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">5</div>
|
||
<div class="flow-body">
|
||
<h4>Spawn initial actor <span class="tag">scheduler.rs</span></h4>
|
||
<p><code>scheduler::spawn(f)</code> pops a slot index from the free list, grabs a stack from the pool (or <code>mmap</code>s one), writes the initial register frame, stores the closure <em>in the slot</em>, bumps <code>live_actors</code>, publishes <code>Queued</code> and enqueues the pid. Details in <a href="#spawn" style="color:var(--accent)">Spawn</a>.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">6</div>
|
||
<div class="flow-body">
|
||
<h4>Spawn N−1 OS scheduler threads, enter <code>schedule_loop</code> on thread 0</h4>
|
||
<p>Each extra thread clones <code>Arc<RuntimeInner></code>, sets its thread-locals, and enters <code>schedule_loop</code>. Thread 0 is the calling thread and blocks in the loop until the program is done.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">7</div>
|
||
<div class="flow-body">
|
||
<h4>Termination & shutdown — counter-based</h4>
|
||
<p>No slot-table scan under a big lock anymore. A thread exits when the queue pops empty <em>and</em> <code>live_actors == 0</code> <em>and</em> no IO is outstanding. <code>live_actors</code> is incremented before each spawn's enqueue and decremented as the <strong>very last</strong> step of <code>finalize_actor</code> — strictly after every wakeup finalize produces — so <code>live == 0</code> proves no work can ever appear again, and every thread independently reaches the same verdict. Then OS threads are joined and <code>IoThread::drop()</code> tears down both background threads.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- THE CYCLE -->
|
||
<section id="cycle">
|
||
<div class="section-label">Core Mechanism</div>
|
||
<h2>The Scheduler Cycle</h2>
|
||
<p>This is the heartbeat of the runtime, drawn the way biochemists draw the Krebs cycle — because it
|
||
<em>is</em> one: a closed loop of state transformations that regenerates its own starting point, fed
|
||
from outside by unparks and drained by finalization. The OS thread's execution context is the
|
||
energy currency: at station 4 it is handed to the actor (<code>switch_to_actor</code>) and at station 5
|
||
the actor hands it back — the bottom arc is dashed because, for that stretch, the scheduler
|
||
doesn't exist on this thread at all.</p>
|
||
|
||
<div class="diagram-wrap">
|
||
<svg width="920" height="700" viewBox="0 0 920 700" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;font-family:'JetBrains Mono',monospace">
|
||
<defs>
|
||
<marker id="kc-teal" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#4ecdc4"/></marker>
|
||
<marker id="kc-blue" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#5b8af5"/></marker>
|
||
<marker id="kc-purple" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#bc8cff"/></marker>
|
||
<marker id="kc-green" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#56d364"/></marker>
|
||
<marker id="kc-orange" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#f5a623"/></marker>
|
||
<marker id="kc-red" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#ff6b6b"/></marker>
|
||
<marker id="kc-dim" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#606880"/></marker>
|
||
</defs>
|
||
|
||
<!-- faint full ring -->
|
||
<circle cx="460" cy="350" r="185" fill="none" stroke="#252a38" stroke-width="1" stroke-dasharray="2,4"/>
|
||
|
||
<!-- center label -->
|
||
<text x="460" y="338" fill="#e8eaf6" font-size="15" font-weight="700" text-anchor="middle">schedule_loop</text>
|
||
<text x="460" y="358" fill="#606880" font-size="9" text-anchor="middle">one revolution ≈ one resume</text>
|
||
|
||
<!-- ring arcs (clockwise). C=(460,350) R=185 -->
|
||
<!-- S1→S2 -->
|
||
<path d="M 396.7 176.2 A 185 185 0 0 1 523.3 176.2" fill="none" stroke="#4ecdc4" stroke-width="1.5" marker-end="url(#kc-blue)"/>
|
||
<!-- S2→S3 -->
|
||
<path d="M 578.9 208.3 A 185 185 0 0 1 640.9 311.5" fill="none" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#kc-purple)"/>
|
||
<!-- S3→S4 -->
|
||
<path d="M 640.9 388.5 A 185 185 0 0 1 583.8 487.5" fill="none" stroke="#bc8cff" stroke-width="1.5" marker-end="url(#kc-green)"/>
|
||
<!-- S4→S5 dashed: OS thread is away, inside the actor -->
|
||
<path d="M 517.2 525.9 A 185 185 0 0 1 402.8 525.9" fill="none" stroke="#56d364" stroke-width="1" stroke-dasharray="3,4"/>
|
||
<!-- S5→S6 -->
|
||
<path d="M 336.2 487.5 A 185 185 0 0 1 279.1 388.5" fill="none" stroke="#f5a623" stroke-width="1.5" marker-end="url(#kc-red)"/>
|
||
<!-- S6→S1 -->
|
||
<path d="M 279.1 311.5 A 185 185 0 0 1 336.2 212.5" fill="none" stroke="#ff6b6b" stroke-width="1.5" marker-end="url(#kc-teal)"/>
|
||
|
||
<!-- S1: drain -->
|
||
<rect x="282" y="152" width="170" height="56" rx="7" fill="#13161e" stroke="#4ecdc4" stroke-width="1.5"/>
|
||
<text x="367" y="172" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">1 · drain (try-lock)</text>
|
||
<text x="367" y="186" fill="#606880" font-size="8" text-anchor="middle">pop due timers · drain IO</text>
|
||
<text x="367" y="198" fill="#606880" font-size="8" text-anchor="middle">one winner per round</text>
|
||
|
||
<!-- S2: pop -->
|
||
<rect x="467" y="152" width="170" height="56" rx="7" fill="#13161e" stroke="#5b8af5" stroke-width="1.5"/>
|
||
<text x="552" y="172" fill="#5b8af5" font-size="10" font-weight="700" text-anchor="middle">2 · pop run queue</text>
|
||
<text x="552" y="186" fill="#606880" font-size="8" text-anchor="middle">None: idle-sleep on wake fd</text>
|
||
<text x="552" y="198" fill="#606880" font-size="8" text-anchor="middle">live==0 ∧ io==0 → AllDone</text>
|
||
|
||
<!-- S3: claim -->
|
||
<rect x="560" y="312" width="170" height="56" rx="7" fill="#13161e" stroke="#bc8cff" stroke-width="1.5"/>
|
||
<text x="645" y="332" fill="#bc8cff" font-size="10" font-weight="700" text-anchor="middle">3 · claim slot</text>
|
||
<text x="645" y="346" fill="#606880" font-size="8" text-anchor="middle">CAS Queued → Running</text>
|
||
<text x="645" y="358" fill="#606880" font-size="8" text-anchor="middle">stale gen → skip pid</text>
|
||
|
||
<!-- S4: arm + handoff -->
|
||
<rect x="467" y="472" width="170" height="56" rx="7" fill="#13161e" stroke="#56d364" stroke-width="1.5"/>
|
||
<text x="552" y="492" fill="#56d364" font-size="10" font-weight="700" text-anchor="middle">4 · arm & hand off</text>
|
||
<text x="552" y="506" fill="#606880" font-size="8" text-anchor="middle">load sp · take closure (1st)</text>
|
||
<text x="552" y="518" fill="#606880" font-size="8" text-anchor="middle">arm timeslice · preempt on</text>
|
||
|
||
<!-- S5: return -->
|
||
<rect x="282" y="472" width="170" height="56" rx="7" fill="#13161e" stroke="#f5a623" stroke-width="1.5"/>
|
||
<text x="367" y="492" fill="#f5a623" font-size="10" font-weight="700" text-anchor="middle">5 · take back</text>
|
||
<text x="367" y="506" fill="#606880" font-size="8" text-anchor="middle">preempt off · store sp</text>
|
||
<text x="367" y="518" fill="#606880" font-size="8" text-anchor="middle">read YieldIntent / done flag</text>
|
||
|
||
<!-- S6: branch -->
|
||
<rect x="190" y="312" width="170" height="56" rx="7" fill="#13161e" stroke="#ff6b6b" stroke-width="1.5"/>
|
||
<text x="275" y="332" fill="#ff6b6b" font-size="10" font-weight="700" text-anchor="middle">6 · settle the pid</text>
|
||
<text x="275" y="346" fill="#606880" font-size="8" text-anchor="middle">Yield → re-queue · Park → CAS</text>
|
||
<text x="275" y="358" fill="#606880" font-size="8" text-anchor="middle">done → finalize_actor</text>
|
||
|
||
<!-- run queue pool, top center -->
|
||
<rect x="350" y="28" width="220" height="56" rx="7" fill="#0d1220" stroke="#5b8af5" stroke-width="1.5"/>
|
||
<text x="460" y="48" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">run queue</text>
|
||
<text x="460" y="62" fill="#606880" font-size="8" text-anchor="middle">rq-mutex | rq-mpmc | rq-striped</text>
|
||
<text x="460" y="74" fill="#606880" font-size="8" text-anchor="middle">a pid is here at most once</text>
|
||
|
||
<!-- pool → S2 -->
|
||
<path d="M 540 84 C 555 105 555 125 552 150" fill="none" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#kc-blue)"/>
|
||
<text x="585" y="118" fill="#606880" font-size="8">pop()</text>
|
||
|
||
<!-- S1 → pool: drain produces unparks -->
|
||
<path d="M 380 150 C 390 125 400 105 420 86" fill="none" stroke="#4ecdc4" stroke-width="1.2" marker-end="url(#kc-blue)"/>
|
||
<text x="388" y="108" fill="#606880" font-size="8" text-anchor="end">unpark_at(pid,e)</text>
|
||
|
||
<!-- external unparks, top right -->
|
||
<rect x="660" y="28" width="220" height="88" rx="7" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="770" y="48" fill="#a8e6cf" font-size="10" font-weight="700" text-anchor="middle">unparks from anywhere</text>
|
||
<text x="770" y="63" fill="#606880" font-size="8" text-anchor="middle">channel send · mutex grant</text>
|
||
<text x="770" y="76" fill="#606880" font-size="8" text-anchor="middle">Down / ExitSignal · joiner wake</text>
|
||
<text x="770" y="89" fill="#606880" font-size="8" text-anchor="middle">epoch-matched: at most one</text>
|
||
<text x="770" y="102" fill="#606880" font-size="8" text-anchor="middle">wake lands per wait</text>
|
||
<path d="M 660 56 L 575 56" fill="none" stroke="#606880" stroke-width="1.2" marker-end="url(#kc-blue)"/>
|
||
|
||
<!-- timers + io inputs, left -->
|
||
<rect x="52" y="128" width="180" height="80" rx="7" fill="#13161e" stroke="#252a38" stroke-width="1.5"/>
|
||
<text x="142" y="148" fill="#a8e6cf" font-size="10" font-weight="700" text-anchor="middle">due work</text>
|
||
<text x="142" y="163" fill="#606880" font-size="8" text-anchor="middle">timer min-heap (Sleep ·</text>
|
||
<text x="142" y="176" fill="#606880" font-size="8" text-anchor="middle">WaitTimeout) · completions:</text>
|
||
<text x="142" y="189" fill="#606880" font-size="8" text-anchor="middle">FdReady · Blocking{result}</text>
|
||
<path d="M 232 172 L 280 178" fill="none" stroke="#606880" stroke-width="1.2" marker-end="url(#kc-teal)"/>
|
||
|
||
<!-- parked capsule, left middle -->
|
||
<rect x="28" y="380" width="142" height="44" rx="22" fill="#0d0f14" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="99" y="398" fill="#c8d0e0" font-size="10" font-weight="700" text-anchor="middle">Parked(epoch)</text>
|
||
<text x="99" y="412" fill="#606880" font-size="8" text-anchor="middle">off the cycle entirely</text>
|
||
<!-- S6 → parked -->
|
||
<path d="M 196 368 L 162 382" fill="none" stroke="#ff6b6b" stroke-width="1.2" marker-end="url(#kc-dim)"/>
|
||
<text x="206" y="384" fill="#606880" font-size="8">Park: CAS</text>
|
||
<!-- parked → pool (dashed, via unpark) -->
|
||
<path d="M 62 378 C 8 300 4 60 348 42" fill="none" stroke="#606880" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#kc-blue)"/>
|
||
<text x="26" y="322" fill="#606880" font-size="8">unpark_at</text>
|
||
|
||
<!-- yield re-queue: S6 → pool -->
|
||
<path d="M 258 310 C 244 210 286 96 348 58" fill="none" stroke="#ff6b6b" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#kc-blue)"/>
|
||
<text x="236" y="228" fill="#606880" font-size="8" text-anchor="end">Yield:</text>
|
||
<text x="236" y="240" fill="#606880" font-size="8" text-anchor="end">enqueue</text>
|
||
|
||
<!-- finalize box, bottom left -->
|
||
<rect x="30" y="560" width="230" height="110" rx="7" fill="#13161e" stroke="#f85149" stroke-width="1.5"/>
|
||
<text x="145" y="580" fill="#f85149" font-size="10" font-weight="700" text-anchor="middle">finalize_actor (leaves cycle)</text>
|
||
<text x="145" y="596" fill="#606880" font-size="8" text-anchor="middle">Down → monitors</text>
|
||
<text x="145" y="609" fill="#606880" font-size="8" text-anchor="middle">ExitSignal / stop cascade → links</text>
|
||
<text x="145" y="622" fill="#606880" font-size="8" text-anchor="middle">Signal → supervisor · wake joiners</text>
|
||
<text x="145" y="635" fill="#606880" font-size="8" text-anchor="middle">stack → pool · slot → Vacant(gen+1)</text>
|
||
<text x="145" y="648" fill="#606880" font-size="8" text-anchor="middle">live_actors −= 1 (last)</text>
|
||
<!-- S6 → finalize -->
|
||
<path d="M 240 370 C 200 430 170 490 150 558" fill="none" stroke="#f85149" stroke-width="1.2" marker-end="url(#kc-red)"/>
|
||
<text x="150" y="470" fill="#606880" font-size="8">done</text>
|
||
|
||
<!-- the actor: the ATP of the cycle -->
|
||
<rect x="340" y="600" width="240" height="80" rx="10" fill="#0d1220" stroke="#a8e6cf" stroke-width="1.5"/>
|
||
<text x="460" y="622" fill="#a8e6cf" font-size="11" font-weight="700" text-anchor="middle">actor runs on its own stack</text>
|
||
<text x="460" y="638" fill="#606880" font-size="8" text-anchor="middle">until: timeslice/yield_now → Yield</text>
|
||
<text x="460" y="651" fill="#606880" font-size="8" text-anchor="middle">recv/lock/sleep/wait_fd → Park</text>
|
||
<text x="460" y="664" fill="#606880" font-size="8" text-anchor="middle">return/panic/stop → done flag</text>
|
||
|
||
<!-- handoff arrows: the ADP↔ATP exchange -->
|
||
<path d="M 540 530 C 530 560 515 580 495 598" fill="none" stroke="#56d364" stroke-width="2" marker-end="url(#kc-green)"/>
|
||
<text x="575" y="548" fill="#56d364" font-size="9">switch_to_actor()</text>
|
||
<path d="M 425 598 C 405 580 390 560 380 530" fill="none" stroke="#f5a623" stroke-width="2" marker-end="url(#kc-orange)"/>
|
||
<text x="345" y="548" fill="#f5a623" font-size="9" text-anchor="end">switch_to_scheduler()</text>
|
||
<text x="460" y="574" fill="#606880" font-size="8" text-anchor="middle">rsp swap · 6 GPRs each way</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<p>Two details worth pausing on. First, the <strong>drain is try-lock</strong>: one winner per revolution
|
||
pops due timers and IO completions; losers skip straight to the queue, so the pure-compute hot
|
||
path never contends a global lock just to learn there's nothing to drain. Second, the
|
||
<strong>claim is a CAS</strong>, <code>Queued → Running</code> on the slot word. The only way it fails is a stale
|
||
generation (the actor died and the slot was recycled while the pid sat in the queue), in which
|
||
case the pid is simply skipped — nothing else can move a queued actor's word, because wakes
|
||
no-op on <code>Queued</code>.</p>
|
||
|
||
<h3>The Yield Sources</h3>
|
||
<table class="state-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Source</th>
|
||
<th>Intent set</th>
|
||
<th>Who re-queues</th>
|
||
<th>Notes</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><code>yield_now()</code></td>
|
||
<td><span class="pill pill-green">Yield</span></td>
|
||
<td>Scheduler immediately</td>
|
||
<td><code>yield_return</code>: Running → Queued, pushed to queue tail</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Allocator preemption</td>
|
||
<td><span class="pill pill-green">Yield</span></td>
|
||
<td>Scheduler immediately</td>
|
||
<td>RDTSC check in <code>maybe_preempt()</code> (every Nth alloc or <code>check!()</code>)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>channel::recv()</code> (empty)</td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td><code>send()</code> → <code>unpark_at(pid, e)</code></td>
|
||
<td><code>begin_wait</code> opens the epoch, then the receiver registers</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>select(arms)</code></td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td>First ready arm's sender</td>
|
||
<td>Loser arms hold a consumed epoch — proven no-ops, not pending wakes</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>mutex::lock()</code> (contended)</td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td>Guard drop <em>or</em> timeout timer</td>
|
||
<td>Grant and timer race through the epoch — exactly one lands</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>sleep(d)</code> / <code>recv_timeout</code></td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td>Timer heap → drain step</td>
|
||
<td>Entry carries the epoch; a satisfied wait's entry is a strict no-op</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>wait_readable/writable(fd)</code></td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td>epoll thread → completions → drain</td>
|
||
<td>EPOLLONESHOT; waiter registered as <code>(pid, epoch)</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>ServerRef::call()</code></td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td>Server's reply send (or its death)</td>
|
||
<td>Just <code>recv()</code> on a fresh one-shot reply channel</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>request_stop(pid)</code> target</td>
|
||
<td><span class="pill pill-red">— (unwinds)</span></td>
|
||
<td>n/a — <code>StopSentinel</code> panic</td>
|
||
<td>Raised at the next <code>check_cancelled()</code>: alloc, <code>check!()</code>, or wake from a park</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- SLOT STATE -->
|
||
<section id="slot-state">
|
||
<div class="section-label">The Loom-Checked Heart</div>
|
||
<h2>Slot State & the Park-Epoch</h2>
|
||
<p>Everything the scheduler needs to know about an actor's schedulability lives in <strong>one atomic
|
||
<code>u64</code></strong> per slot: <code>(generation << 32) | (park-epoch << 8) | state</code>. Every transition
|
||
is a CAS on the whole packed word, which buys two things at once: the generation check is atomic
|
||
with the transition (a stale pid can never act on a recycled slot), and an unpark racing the
|
||
prep-to-park window becomes a <em>state</em> — <code>RunningNotified</code> — resolved by the scheduler's
|
||
park-return CAS rather than a flag read under a lock.</p>
|
||
|
||
<div class="diagram-wrap">
|
||
<svg width="860" height="545" viewBox="0 0 860 545" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;font-family:'JetBrains Mono',monospace">
|
||
<defs>
|
||
<marker id="ss-blue" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#5b8af5"/></marker>
|
||
<marker id="ss-green" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#56d364"/></marker>
|
||
<marker id="ss-orange" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#f5a623"/></marker>
|
||
<marker id="ss-red" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#f85149"/></marker>
|
||
<marker id="ss-purple" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#bc8cff"/></marker>
|
||
<marker id="ss-dim" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#606880"/></marker>
|
||
</defs>
|
||
|
||
<text x="430" y="26" fill="#606880" font-size="9" text-anchor="middle" letter-spacing="2">ONE ATOMIC u64 PER SLOT — EVERY TRANSITION IS A CAS ON THE WHOLE WORD</text>
|
||
<rect x="200" y="38" width="190" height="34" fill="#0d1220" stroke="#5b8af5" stroke-width="1.5"/>
|
||
<text x="295" y="59" fill="#5b8af5" font-size="10" text-anchor="middle">generation (32b)</text>
|
||
<rect x="390" y="38" width="170" height="34" fill="#0d1220" stroke="#bc8cff" stroke-width="1.5"/>
|
||
<text x="475" y="59" fill="#bc8cff" font-size="10" text-anchor="middle">park-epoch (24b)</text>
|
||
<rect x="560" y="38" width="100" height="34" fill="#0d1220" stroke="#4ecdc4" stroke-width="1.5"/>
|
||
<text x="610" y="59" fill="#4ecdc4" font-size="10" text-anchor="middle">state (8b)</text>
|
||
<text x="295" y="86" fill="#606880" font-size="8" text-anchor="middle">checked atomically: no ABA</text>
|
||
<text x="500" y="86" fill="#606880" font-size="8" text-anchor="middle">wait identity</text>
|
||
|
||
<!-- states -->
|
||
<rect x="50" y="210" width="120" height="46" rx="23" fill="#0d0f14" stroke="#606880" stroke-width="1.5"/>
|
||
<text x="110" y="230" fill="#c8d0e0" font-size="11" font-weight="700" text-anchor="middle">Vacant</text>
|
||
<text x="110" y="245" fill="#606880" font-size="8" text-anchor="middle">in free list</text>
|
||
|
||
<rect x="270" y="210" width="130" height="46" rx="23" fill="#0d1220" stroke="#5b8af5" stroke-width="1.5"/>
|
||
<text x="335" y="230" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">Queued</text>
|
||
<text x="335" y="245" fill="#606880" font-size="8" text-anchor="middle">in run queue, once</text>
|
||
|
||
<rect x="540" y="210" width="130" height="46" rx="23" fill="#0d1220" stroke="#56d364" stroke-width="1.5"/>
|
||
<text x="605" y="230" fill="#56d364" font-size="11" font-weight="700" text-anchor="middle">Running</text>
|
||
<text x="605" y="245" fill="#606880" font-size="8" text-anchor="middle">on an OS thread</text>
|
||
|
||
<rect x="650" y="320" width="180" height="46" rx="23" fill="#0d1220" stroke="#f5a623" stroke-width="1.5"/>
|
||
<text x="740" y="340" fill="#f5a623" font-size="11" font-weight="700" text-anchor="middle">RunningNotified</text>
|
||
<text x="740" y="355" fill="#606880" font-size="8" text-anchor="middle">wake landed mid-run</text>
|
||
|
||
<rect x="540" y="410" width="130" height="46" rx="23" fill="#0d0f14" stroke="#bc8cff" stroke-width="1.5"/>
|
||
<text x="605" y="430" fill="#bc8cff" font-size="11" font-weight="700" text-anchor="middle">Parked</text>
|
||
<text x="605" y="445" fill="#606880" font-size="8" text-anchor="middle">off the run queue</text>
|
||
|
||
<rect x="270" y="410" width="130" height="46" rx="23" fill="#0d0f14" stroke="#f85149" stroke-width="1.5"/>
|
||
<text x="335" y="430" fill="#f85149" font-size="11" font-weight="700" text-anchor="middle">Done</text>
|
||
<text x="335" y="445" fill="#606880" font-size="8" text-anchor="middle">return · panic · stop</text>
|
||
|
||
<!-- transitions -->
|
||
<path d="M 170 233 L 268 233" fill="none" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#ss-blue)"/>
|
||
<text x="219" y="225" fill="#606880" font-size="8" text-anchor="middle">spawn: publish</text>
|
||
<text x="219" y="248" fill="#606880" font-size="8" text-anchor="middle">e = 0</text>
|
||
|
||
<path d="M 400 224 L 538 224" fill="none" stroke="#56d364" stroke-width="1.5" marker-end="url(#ss-green)"/>
|
||
<text x="469" y="216" fill="#606880" font-size="8" text-anchor="middle">pop: try_claim</text>
|
||
<path d="M 538 242 L 400 242" fill="none" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#ss-blue)"/>
|
||
<text x="469" y="256" fill="#606880" font-size="8" text-anchor="middle">yield_return</text>
|
||
|
||
<path d="M 575 208 C 588 172 642 172 634 206" fill="none" stroke="#bc8cff" stroke-width="1.2" stroke-dasharray="3,2" marker-end="url(#ss-purple)"/>
|
||
<text x="652" y="178" fill="#606880" font-size="8">begin_wait: e+1, then register</text>
|
||
<text x="652" y="190" fill="#606880" font-size="8">(pid, e) with wakers</text>
|
||
|
||
<path d="M 605 256 L 605 408" fill="none" stroke="#bc8cff" stroke-width="1.5" marker-end="url(#ss-purple)"/>
|
||
<text x="597" y="328" fill="#606880" font-size="8" text-anchor="end">park_return:</text>
|
||
<text x="597" y="340" fill="#606880" font-size="8" text-anchor="end">e preserved</text>
|
||
|
||
<path d="M 648 256 C 700 270 725 290 738 318" fill="none" stroke="#f5a623" stroke-width="1.5" marker-end="url(#ss-orange)"/>
|
||
<text x="722" y="276" fill="#606880" font-size="8">unpark mid-run:</text>
|
||
<text x="722" y="288" fill="#606880" font-size="8">e consumed (e+1)</text>
|
||
|
||
<path d="M 650 352 C 480 372 420 310 396 252" fill="none" stroke="#f5a623" stroke-width="1.5" stroke-dasharray="5,3" marker-end="url(#ss-blue)"/>
|
||
<text x="596" y="382" fill="#606880" font-size="8" text-anchor="end">park_return here:</text>
|
||
<text x="596" y="394" fill="#606880" font-size="8" text-anchor="end">re-queue, no park</text>
|
||
|
||
<path d="M 540 433 C 430 420 365 330 344 258" fill="none" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#ss-blue)"/>
|
||
<text x="470" y="478" fill="#606880" font-size="8" text-anchor="middle">unpark_at(e): e+1,</text>
|
||
<text x="470" y="490" fill="#606880" font-size="8" text-anchor="middle">waker enqueues</text>
|
||
<path d="M 470 466 L 470 428" fill="none" stroke="#252a38" stroke-width="1"/>
|
||
|
||
<path d="M 560 256 C 480 310 400 360 372 408" fill="none" stroke="#f85149" stroke-width="1.5" marker-end="url(#ss-red)"/>
|
||
<text x="498" y="286" fill="#606880" font-size="8" text-anchor="end">set_done: e=0</text>
|
||
|
||
<path d="M 268 433 C 150 420 95 340 106 258" fill="none" stroke="#606880" stroke-width="1.5" marker-end="url(#ss-dim)"/>
|
||
<text x="92" y="330" fill="#606880" font-size="8" text-anchor="end">reclaim: gen+1,</text>
|
||
<text x="92" y="342" fill="#606880" font-size="8" text-anchor="end">stale pids</text>
|
||
<text x="92" y="354" fill="#606880" font-size="8" text-anchor="end">die here</text>
|
||
|
||
<text x="430" y="518" fill="#a8e6cf" font-size="9" text-anchor="middle">every successful wake consumes the epoch ⇒ at most one wake lands per wait, by construction</text>
|
||
<text x="430" y="534" fill="#606880" font-size="8" text-anchor="middle">loom model-checks these transitions: lost wakeup, double enqueue, stale epoch, ABA — all unreachable</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<p>The <strong>park-epoch</strong> (middle 24 bits) is the actor's <em>wait identity</em>. A waiting actor calls
|
||
<code>begin_wait</code> once per wait — bump, get <code>e</code> — <em>before</em> registering <code>(pid, e)</code> with any
|
||
waker. Wakes are epoch-matched: <code>unpark_at(pid, e)</code> lands only if the word still carries
|
||
<code>e</code>, and every successful wake <em>consumes</em> the epoch. So at most one wake can ever land per
|
||
wait, by construction. This is what lets a <code>select</code> register with several channels and park
|
||
once: the winning arm's wake bumps the epoch, and every loser arm's later wake fails the match
|
||
and no-ops — instead of leaving a phantom notification that would fault the actor's next
|
||
one-shot park in <code>sleep</code>, <code>lock_timeout</code>, or <code>block_on_io</code>.</p>
|
||
<p>The only <strong>wildcard</strong> wake (no epoch) is <code>request_stop</code>, which is terminal — control never
|
||
returns to the code that parked. A no-park exit (a <code>select</code> arm ready at registration time)
|
||
retires the wait explicitly: bump the epoch, then <code>clear_notify</code> eats anything that already
|
||
landed, then re-check the stop flag so a terminal wake can't be swallowed.</p>
|
||
|
||
<div class="callout">
|
||
<span class="callout-icon">🔬</span>
|
||
<p><code>slot_state.rs</code> compiles its atomics from <code>sync_shim</code>, so <strong>loom model-checks the
|
||
production transitions directly</strong> (<code>RUSTFLAGS="--cfg loom"</code>). The shipped theorems: no lost
|
||
wakeup (park vs unpark), at-most-one enqueue (two racing unparkers), consumed-epoch wakes never
|
||
land (select's loser arms), the retire eats late arm notifications, stale-generation unparks
|
||
never touch a reused slot, and unpark-vs-claim coalesces. Each is an exhaustive interleaving
|
||
proof, not a stress test.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- RUN QUEUE -->
|
||
<section id="run-queue">
|
||
<div class="section-label">Scheduling</div>
|
||
<h2>Run Queue: Three Variants, One Contract</h2>
|
||
<p>The v0.3 doc listed the single global <code>Mutex<SharedState></code> as the primary scalability
|
||
ceiling. v0.8's answer is to make the queue its own module behind a <strong>compile-time feature
|
||
choice</strong>, all three variants loom-checked against the same contract — and to shrink what the
|
||
queue mutex covers to <em>only the pop/push itself</em>; the slot's own atomics carry everything
|
||
needed to resume.</p>
|
||
|
||
<div class="module-grid">
|
||
<div class="module-card">
|
||
<div class="module-name">rq-mutex <span class="pill pill-blue">default</span></div>
|
||
<p>A <code>VecDeque</code> behind a <code>RawMutex</code>. Innermost lock in the ordering — nothing else is ever acquired under it. Simple, predictable, and fine until queue ops dominate.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">rq-mpmc</div>
|
||
<p>A Vyukov-style bounded MPMC ring with per-cell sequence counters. Lock-free push/pop; falls back to an overflow side-list to keep push infallible.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">rq-striped</div>
|
||
<p>M independent rings; producers and consumers hash/rotate across stripes to spread contention. A stepping stone toward per-thread deques with work stealing.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">the contract</div>
|
||
<p><strong>Push is infallible. A pid is in the queue at most once.</strong> The only pushes are paired 1:1 with successful transitions <em>into</em> <code>Queued</code>; only the scheduler transitions <code>Queued → Running</code>, paired 1:1 with pops. Queue ops require preemption disabled (debug-asserted).</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="callout">
|
||
<span class="callout-icon">📎</span>
|
||
<p>Termination doesn't lean on pop-<code>None</code> being a fence (with the rings it's only a snapshot). The argument is counter-first: every queue entry's target stays <code>Queued</code> — hence counted in <code>live_actors</code> — until that very entry is popped, so <code>live == 0</code> by itself implies the queue is empty forever.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- SPAWN WALKTHROUGH -->
|
||
<section id="spawn">
|
||
<div class="section-label">Spawn Mechanics</div>
|
||
<h2>New Actor From First Resume</h2>
|
||
<p>Spawning is still the trickiest part of the runtime: an actor's first resume is fundamentally
|
||
different from subsequent ones because we can't "call" into a new stack — we have to
|
||
<code>ret</code> into it.</p>
|
||
|
||
<div class="flow-diagram">
|
||
<div class="flow-step">
|
||
<div class="flow-num">1</div>
|
||
<div class="flow-body">
|
||
<h4><code>scheduler::spawn(f)</code> called</h4>
|
||
<p>Pops a slot index from the free list (<code>RawMutex<Vec<u32>></code>) — the slab is fixed, so slot exhaustion panics loudly naming <code>Config::max_actors</code>. The slot's current generation becomes the new <code>Pid(index, gen)</code>. A <code>Stack</code> comes from the <strong>stack pool</strong> if one is cached, else a fresh 64 KB <code>mmap</code> + guard page.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">2</div>
|
||
<div class="flow-body">
|
||
<h4>Initial stack frame written <span class="tag">context::init_actor_stack()</span></h4>
|
||
<p>Starting from <code>top & ~15 − 8</code> (aligned), pushes downward: the <code>trampoline</code> address as the <code>ret</code> target, then 6 zero words for the callee-saved registers. The resulting <code>rsp</code> is stored in the slot's <code>sp</code> atomic. No actual function call has happened yet.</p>
|
||
<pre><code>high addr ← top
|
||
top-8: &trampoline ← will be popped by 'ret'
|
||
top-16: 0 ← rbx
|
||
top-24: 0 ← rbp
|
||
top-32: 0 ← r12
|
||
top-40: 0 ← r13
|
||
top-48: 0 ← r14
|
||
top-56: 0 ← r15 ← initial rsp stored here</code></pre>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">3</div>
|
||
<div class="flow-body">
|
||
<h4>Closure stored <em>in the slot</em></h4>
|
||
<p>The <code>Box<dyn FnOnce() + Send></code> goes into the slot's <code>closure: AtomicPtr</code> — swap-to-take, no map, no lock. (v0.3 kept a <code>pending_closures</code> map inside <code>SharedState</code>; that's gone with the struct.) It can't ride the actor's stack because nothing can pass it via a register during first resume.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">4</div>
|
||
<div class="flow-body">
|
||
<h4>Count it live, then publish</h4>
|
||
<p><code>live_actors += 1</code> <em>before</em> the actor becomes poppable — the termination argument needs every enqueue to target a counted actor. Then <code>publish_queued(gen)</code>: a plain Release store <code>Vacant → Queued</code> with epoch 0 (the spawner owns the vacant slot exclusively, so no CAS needed). This store is the moment the actor exists to pops, unparks and stops. Finally the pid is enqueued.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">5</div>
|
||
<div class="flow-body">
|
||
<h4>Scheduler pops the pid, claims, prepares first resume</h4>
|
||
<p><code>try_claim</code> CASes <code>Queued → Running</code>. The scheduler loads <code>sp</code> from the slot, sees <code>take_closure()</code> return <code>Some</code> (it's the first resume) and moves the box into the trampoline's thread-local. Then: bind the stop flag, reset the timeslice, enable preemption, <code>switch_to_actor()</code>.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">6</div>
|
||
<div class="flow-body">
|
||
<h4>First context switch lands in <code>trampoline()</code></h4>
|
||
<p><code>switch_to_actor()</code> saves the scheduler's GPRs, loads the slot's <code>sp</code> as the new <code>rsp</code>, pops the 6 zero words, then <code>ret</code>s — popping the trampoline address and jumping to it. We're now on the actor's stack. The trampoline takes the closure from the thread-local and calls it inside <code>catch_unwind(AssertUnwindSafe(f))</code>.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">7</div>
|
||
<div class="flow-body">
|
||
<h4>Actor returns → trampoline classifies the outcome</h4>
|
||
<p><code>Ok(())</code> → <code>Exit</code>. <code>Err(payload)</code> → <code>Panic(payload)</code> — <em>unless</em> the payload is the <code>StopSentinel</code>, in which case the outcome is <code>Stopped</code>: a cooperative cancellation, not a crash, and links/supervisors treat it as such. The done flag is set, one last <code>switch_to_scheduler()</code>, and the scheduler runs <code>finalize_actor</code>: Down to monitors, ExitSignal or stop cascade to links, Signal to the supervisor, epoch-matched joiner wakes, stack back to the pool, slot reclaimed at <code>gen+1</code>, and <code>live_actors −= 1</code> last of all.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- PREEMPTION -->
|
||
<section id="preempt">
|
||
<div class="section-label">Preemption & Cancellation</div>
|
||
<h2>Allocator-Driven Timeslicing</h2>
|
||
|
||
<div class="two-col">
|
||
<div>
|
||
<h3>How it works</h3>
|
||
<p>The <code>PreemptingAllocator</code> is installed as the process's <code>#[global_allocator]</code>. Its <code>alloc()</code>, <code>alloc_zeroed()</code>, and <code>realloc()</code> all call <code>maybe_preempt()</code> before delegating to the system allocator.</p>
|
||
<p><code>maybe_preempt()</code> decrements a thread-local counter. Every <strong>128 allocations</strong> (default), it reads RDTSC. If <code>rdtsc() − timeslice_start > 300_000 cycles</code> (~100µs at 3 GHz) and <code>PREEMPTION_ENABLED == true</code>, it calls <code>switch_to_scheduler()</code>.</p>
|
||
<p>The <code>check!()</code> macro calls the same <code>maybe_preempt()</code> — the explicit preemption point for tight no-alloc loops, since stable Rust offers no transparent way to interrupt them.</p>
|
||
<h3>Cooperative cancellation rides the same rail</h3>
|
||
<p><code>request_stop(pid)</code> sets a flag in the target's <code>Arc<AtomicBool></code> and fires a wildcard unpark. The flag is observed by <code>check_cancelled()</code> — called from <code>maybe_preempt()</code> and from the wake side of every park — which raises the <code>StopSentinel</code> panic. The trampoline's <code>catch_unwind</code> recognises it and reports <code>Outcome::Stopped</code>, distinct from a user <code>Panic</code>: the stack unwinds, <code>Drop</code>s run, links and supervisors see a stop, not a crash.</p>
|
||
</div>
|
||
<div>
|
||
<h3>Invariant: preemption off while holding smarm-internal locks</h3>
|
||
<p>A timeslice switch while holding an internal lock would suspend the actor with the lock held, stalling every OS thread that touches it. With the big <code>SharedState</code> mutex gone, the rule is enforced structurally:</p>
|
||
<ul style="color:var(--text);font-size:0.88rem;padding-left:1.2rem;margin-bottom:1rem;">
|
||
<li style="margin-bottom:0.4rem;"><code>PREEMPTION_ENABLED = false</code> in the scheduler loop before/after <code>switch_to_actor()</code></li>
|
||
<li style="margin-bottom:0.4rem;">every <code>RawMutex</code> guard (slot cold data, free list, stack pool, registry) enters <code>NoPreempt</code> on construction</li>
|
||
<li style="margin-bottom:0.4rem;">run-queue ops require preemption disabled — debug-asserted in <code>run_queue.rs</code></li>
|
||
<li><code>trace::record()</code> also disables preemption (it can allocate)</li>
|
||
</ul>
|
||
<p>Disabling preemption also gates the stop sentinel: with <code>PREEMPTION_ENABLED == false</code>, <code>maybe_preempt</code> neither yields <em>nor raises the sentinel</em>, so no allocation inside a critical section can unwind it. That's why <code>RawMutex</code> can afford to be non-poisoning.</p>
|
||
<div class="callout" style="margin-top:0">
|
||
<span class="callout-icon">⚠</span>
|
||
<p class="warn">Known gap, unchanged: tight no-alloc loops are invisible without explicit <code>check!()</code> calls — to preemption <em>and</em> to cancellation. Documented and by design; such loops are uncommon in message-passing workloads.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<pre><code><span class="cm">// preempt.rs — simplified</span>
|
||
<span class="kw">pub</span> <span class="kw">fn</span> <span class="fn">maybe_preempt</span>() {
|
||
ALLOC_COUNT.<span class="fn">with</span>(|c| {
|
||
<span class="kw">let</span> n = c.<span class="fn">get</span>();
|
||
<span class="kw">if</span> n == <span class="nu">0</span> {
|
||
c.<span class="fn">set</span>(ACTIVE_ALLOC_INTERVAL.<span class="fn">with</span>(|i| i.<span class="fn">get</span>())); <span class="cm">// reset counter</span>
|
||
<span class="kw">if</span> PREEMPTION_ENABLED.<span class="fn">with</span>(|e| e.<span class="fn">get</span>()) {
|
||
<span class="fn">check_cancelled</span>(); <span class="cm">// may raise StopSentinel → Outcome::Stopped</span>
|
||
<span class="kw">let</span> elapsed = <span class="fn">rdtsc</span>() - TIMESLICE_START.<span class="fn">with</span>(|s| s.<span class="fn">get</span>());
|
||
<span class="kw">if</span> elapsed > ACTIVE_TIMESLICE_CYCLES.<span class="fn">with</span>(|i| i.<span class="fn">get</span>()) {
|
||
<span class="kw">unsafe</span> { <span class="fn">switch_to_scheduler</span>() }; <span class="cm">// YieldIntent::Yield</span>
|
||
}
|
||
}
|
||
} <span class="kw">else</span> {
|
||
c.<span class="fn">set</span>(n - <span class="nu">1</span>);
|
||
}
|
||
});
|
||
}</code></pre>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- IO -->
|
||
<section id="io">
|
||
<div class="section-label">IO Architecture</div>
|
||
<h2>Two Background Threads, One Wake Pipe</h2>
|
||
|
||
<div class="diagram-wrap">
|
||
<svg width="820" height="300" viewBox="0 0 820 300" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;font-family:'JetBrains Mono',monospace">
|
||
<defs>
|
||
<marker id="ai" markerWidth="7" markerHeight="7" refX="5" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L7,3 z" fill="#4ecdc4"></path>
|
||
</marker>
|
||
<marker id="ai2" markerWidth="7" markerHeight="7" refX="5" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L7,3 z" fill="#f5a623"></path>
|
||
</marker>
|
||
</defs>
|
||
|
||
<!-- Actor -->
|
||
<rect x="20" y="100" width="120" height="110" rx="8" fill="#13161e" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="80" y="122" fill="#a8e6cf" font-size="11" font-weight="700" text-anchor="middle">Actor</text>
|
||
<text x="80" y="140" fill="#606880" font-size="8" text-anchor="middle">calls wait_readable(fd)</text>
|
||
<text x="80" y="155" fill="#606880" font-size="8" text-anchor="middle">or block_on_io(f)</text>
|
||
<text x="80" y="175" fill="#ff6b6b" font-size="8" text-anchor="middle">→ begin_wait: epoch e</text>
|
||
<text x="80" y="190" fill="#ff6b6b" font-size="8" text-anchor="middle">→ register (pid, e), park</text>
|
||
|
||
<!-- Epoll thread -->
|
||
<rect x="220" y="20" width="160" height="120" rx="8" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="300" y="42" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">epoll thread</text>
|
||
<text x="300" y="60" fill="#606880" font-size="8" text-anchor="middle">epoll_wait(-1) loop</text>
|
||
<text x="300" y="75" fill="#606880" font-size="8" text-anchor="middle">EPOLLONESHOT per fd</text>
|
||
<text x="300" y="90" fill="#606880" font-size="8" text-anchor="middle">on ready: push FdReady</text>
|
||
<text x="300" y="105" fill="#606880" font-size="8" text-anchor="middle">write wake_pipe</text>
|
||
<text x="300" y="120" fill="#606880" font-size="8" text-anchor="middle">on shutdown pipe: exit</text>
|
||
|
||
<!-- Pool thread -->
|
||
<rect x="220" y="170" width="160" height="110" rx="8" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="300" y="192" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">pool thread</text>
|
||
<text x="300" y="210" fill="#606880" font-size="8" text-anchor="middle">mpsc::recv() loop</text>
|
||
<text x="300" y="225" fill="#606880" font-size="8" text-anchor="middle">catch_unwind(closure)</text>
|
||
<text x="300" y="240" fill="#606880" font-size="8" text-anchor="middle">push Blocking result</text>
|
||
<text x="300" y="255" fill="#606880" font-size="8" text-anchor="middle">write wake_pipe</text>
|
||
<text x="300" y="268" fill="#606880" font-size="8" text-anchor="middle">tx drop → exit</text>
|
||
|
||
<!-- Completions queue -->
|
||
<rect x="460" y="95" width="150" height="110" rx="8" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="535" y="117" fill="#4ecdc4" font-size="11" font-weight="700" text-anchor="middle">completions</text>
|
||
<text x="535" y="135" fill="#606880" font-size="8" text-anchor="middle">Arc<Mutex<VecDeque>></text>
|
||
<text x="535" y="153" fill="#606880" font-size="8" text-anchor="middle">FdReady { fd, events }</text>
|
||
<text x="535" y="168" fill="#606880" font-size="8" text-anchor="middle">Blocking { pid, e, result }</text>
|
||
<text x="535" y="185" fill="#a8e6cf" font-size="8" text-anchor="middle">drained by schedule_loop</text>
|
||
|
||
<!-- Scheduler -->
|
||
<rect x="680" y="80" width="120" height="140" rx="8" fill="#0d1220" stroke="#5b8af5" stroke-width="1.5"></rect>
|
||
<text x="740" y="102" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">scheduler</text>
|
||
<text x="740" y="120" fill="#606880" font-size="8" text-anchor="middle">poll(wake_fd)</text>
|
||
<text x="740" y="135" fill="#606880" font-size="8" text-anchor="middle">drain completions</text>
|
||
<text x="740" y="150" fill="#606880" font-size="8" text-anchor="middle">FdReady →</text>
|
||
<text x="740" y="162" fill="#606880" font-size="8" text-anchor="middle">resolve waiters[fd]</text>
|
||
<text x="740" y="177" fill="#606880" font-size="8" text-anchor="middle">unpark_at(pid, e)</text>
|
||
<text x="740" y="192" fill="#606880" font-size="8" text-anchor="middle">Blocking → stash in</text>
|
||
<text x="740" y="204" fill="#606880" font-size="8" text-anchor="middle">cold, unpark_at(pid, e)</text>
|
||
|
||
<!-- Arrows -->
|
||
<line x1="140" y1="120" x2="220" y2="75" stroke="#4ecdc4" stroke-width="1" marker-end="url(#ai)"></line>
|
||
<text x="185" y="90" fill="#606880" font-size="7" text-anchor="middle">epoll_ctl ADD</text>
|
||
|
||
<line x1="140" y1="190" x2="220" y2="220" stroke="#4ecdc4" stroke-width="1" marker-end="url(#ai)"></line>
|
||
<text x="182" y="215" fill="#606880" font-size="7" text-anchor="middle">submit(closure)</text>
|
||
|
||
<line x1="380" y1="80" x2="460" y2="130" stroke="#f5a623" stroke-width="1" marker-end="url(#ai2)"></line>
|
||
<line x1="380" y1="220" x2="460" y2="170" stroke="#f5a623" stroke-width="1" marker-end="url(#ai2)"></line>
|
||
|
||
<line x1="610" y1="150" x2="680" y2="150" stroke="#4ecdc4" stroke-width="1.5" marker-end="url(#ai)"></line>
|
||
<text x="645" y="144" fill="#606880" font-size="7" text-anchor="middle">drain</text>
|
||
|
||
<line x1="380" y1="100" x2="680" y2="120" stroke="#f5a623" stroke-width="1" stroke-dasharray="3,2" marker-end="url(#ai2)"></line>
|
||
<line x1="380" y1="240" x2="680" y2="175" stroke="#f5a623" stroke-width="1" stroke-dasharray="3,2" marker-end="url(#ai2)"></line>
|
||
<text x="560" y="105" fill="#606880" font-size="7" text-anchor="middle">wake pipe write</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<p>Both completion kinds now carry the waiter's <strong>park-epoch</strong>, so the wake side is the same
|
||
protocol as everything else. For <code>Blocking</code>, the result is stashed under the slot's cold lock
|
||
<em>before</em> the <code>unpark_at</code> — and if the actor died with the op in flight (a stop), the
|
||
generation check fails and the result is discarded instead of being written into a recycled slot.
|
||
This path closed a <em>latent</em> lost wakeup: the v0.3 code set the result for a still-Running actor
|
||
without flagging it; under v0.8 that interleaving lands as <code>RunningNotified</code> and the upcoming
|
||
park re-queues.</p>
|
||
|
||
<div class="callout">
|
||
<span class="callout-icon">📎</span>
|
||
<p>epoll_ctl ADD/DEL is called by the <strong>scheduler thread</strong> directly on the epollfd — legal per <code>epoll_ctl(2)</code> even while the epoll thread is inside <code>epoll_wait</code>. Avoids a second command channel. The v0.3 fd-leak on actor-death-during-wait is fixed: an unwind guard in <code>wait_fd</code> deregisters on the stop path, and registration does a belt-and-braces <code>EPOLL_CTL_DEL</code> first (harmless ENOENT if absent).</p>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- GENSERVER -->
|
||
<section id="genserver">
|
||
<div class="section-label">OTP — Request / Reply</div>
|
||
<h2>gen_server: One Object Above, One Actor Below</h2>
|
||
<p>The mental model you program against is a synchronous object: a state value with handlers,
|
||
processing one message at a time — no locks, no races, because there's no concurrency <em>inside</em>
|
||
the plane. The implementation is one actor, one inbox channel, and a <code>select</code> loop. The
|
||
diagram maps each promise of plane A to the mechanism in plane B that pays for it.</p>
|
||
|
||
<div class="diagram-wrap">
|
||
<svg width="900" height="700" viewBox="0 0 900 700" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;font-family:'JetBrains Mono',monospace">
|
||
<defs>
|
||
<marker id="gp-blue" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#5b8af5"/></marker>
|
||
<marker id="gp-teal" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#4ecdc4"/></marker>
|
||
<marker id="gp-orange" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#f5a623"/></marker>
|
||
<marker id="gp-green" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#a8e6cf"/></marker>
|
||
</defs>
|
||
|
||
<!-- ===== PLANE A: the model ===== -->
|
||
<polygon points="200,50 840,50 760,240 120,240" fill="rgba(91,138,245,0.04)" stroke="#3a4060" stroke-width="1" stroke-dasharray="6,4"/>
|
||
<text x="120" y="38" fill="#5b8af5" font-size="10" font-weight="700" letter-spacing="2">PLANE A — THE MODEL YOU PROGRAM AGAINST</text>
|
||
|
||
<!-- caller -->
|
||
<rect x="165" y="100" width="130" height="80" rx="7" fill="#13161e" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="230" y="120" fill="#e8eaf6" font-size="10" font-weight="700" text-anchor="middle">caller</text>
|
||
<text x="230" y="136" fill="#606880" font-size="8" text-anchor="middle">call(req) → blocks,</text>
|
||
<text x="230" y="148" fill="#606880" font-size="8" text-anchor="middle">gets Reply back</text>
|
||
<text x="230" y="162" fill="#606880" font-size="8" text-anchor="middle">cast(msg) → fire&forget</text>
|
||
|
||
<!-- server -->
|
||
<rect x="395" y="80" width="210" height="120" rx="7" fill="#0d1220" stroke="#5b8af5" stroke-width="1.5"/>
|
||
<text x="500" y="100" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">GenServer</text>
|
||
<text x="500" y="116" fill="#606880" font-size="8" text-anchor="middle">owned state, one message at a time</text>
|
||
<text x="500" y="132" fill="#c8d0e0" font-size="8" text-anchor="middle">handle_call(Call) → Reply</text>
|
||
<text x="500" y="145" fill="#c8d0e0" font-size="8" text-anchor="middle">handle_cast(Cast)</text>
|
||
<text x="500" y="158" fill="#c8d0e0" font-size="8" text-anchor="middle">handle_info(Info) · handle_down(Down)</text>
|
||
<text x="500" y="171" fill="#c8d0e0" font-size="8" text-anchor="middle">init(ctx) · terminate()</text>
|
||
<text x="500" y="188" fill="#606880" font-size="8" text-anchor="middle">no locks, no races: it's sequential</text>
|
||
|
||
<!-- the world -->
|
||
<rect x="655" y="100" width="150" height="80" rx="7" fill="#13161e" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="730" y="120" fill="#e8eaf6" font-size="10" font-weight="700" text-anchor="middle">the world</text>
|
||
<text x="730" y="136" fill="#606880" font-size="8" text-anchor="middle">out-of-band events → Info</text>
|
||
<text x="730" y="148" fill="#606880" font-size="8" text-anchor="middle">watched pid dies → Down</text>
|
||
<text x="730" y="162" fill="#606880" font-size="8" text-anchor="middle">(system messages first)</text>
|
||
|
||
<!-- plane A arrows -->
|
||
<path d="M 295 125 L 393 125" fill="none" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#gp-blue)"/>
|
||
<path d="M 393 140 L 295 140" fill="none" stroke="#4ecdc4" stroke-width="1.5" marker-end="url(#gp-teal)"/>
|
||
<text x="344" y="118" fill="#606880" font-size="7" text-anchor="middle">call</text>
|
||
<text x="344" y="153" fill="#606880" font-size="7" text-anchor="middle">reply</text>
|
||
<path d="M 295 165 L 393 175" fill="none" stroke="#5b8af5" stroke-width="1" stroke-dasharray="3,2" marker-end="url(#gp-blue)"/>
|
||
<text x="344" y="182" fill="#606880" font-size="7" text-anchor="middle">cast</text>
|
||
<path d="M 653 140 L 607 140" fill="none" stroke="#f5a623" stroke-width="1.2" marker-end="url(#gp-orange)"/>
|
||
|
||
<!-- ===== mapping arrows ===== -->
|
||
<path d="M 230 182 L 230 455" fill="none" stroke="#f5a623" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#gp-orange)"/>
|
||
<text x="238" y="330" fill="#f5a623" font-size="8">call() = send + park</text>
|
||
|
||
<path d="M 430 202 C 370 320 360 430 385 538" fill="none" stroke="#f5a623" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#gp-orange)"/>
|
||
<text x="350" y="414" fill="#f5a623" font-size="8" text-anchor="end">each call carries a</text>
|
||
<text x="350" y="426" fill="#f5a623" font-size="8" text-anchor="end">fresh one-shot reply_tx</text>
|
||
|
||
<path d="M 500 202 L 440 463" fill="none" stroke="#f5a623" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#gp-orange)"/>
|
||
<text x="545" y="328" fill="#f5a623" font-size="8" text-anchor="middle">ONE shared inbox:</text>
|
||
<text x="545" y="340" fill="#f5a623" font-size="8" text-anchor="middle">calls ∥ casts ordered</text>
|
||
|
||
<path d="M 590 202 L 650 438" fill="none" stroke="#f5a623" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#gp-orange)"/>
|
||
<text x="640" y="300" fill="#f5a623" font-size="8">dispatch by Envelope arm</text>
|
||
|
||
<path d="M 730 182 C 810 300 820 450 740 583" fill="none" stroke="#f5a623" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#gp-orange)"/>
|
||
<text x="810" y="390" fill="#f5a623" font-size="8" text-anchor="start">separate</text>
|
||
<text x="810" y="402" fill="#f5a623" font-size="8" text-anchor="start">channels,</text>
|
||
<text x="810" y="414" fill="#f5a623" font-size="8" text-anchor="start">selected</text>
|
||
<text x="810" y="426" fill="#f5a623" font-size="8" text-anchor="start">first</text>
|
||
|
||
<!-- ===== PLANE B: the implementation ===== -->
|
||
<polygon points="200,400 840,400 760,650 120,650" fill="rgba(78,205,196,0.04)" stroke="#3a4060" stroke-width="1" stroke-dasharray="6,4"/>
|
||
<text x="120" y="678" fill="#4ecdc4" font-size="10" font-weight="700" letter-spacing="2">PLANE B — WHAT ACTUALLY RUNS: ONE ACTOR + CHANNELS</text>
|
||
|
||
<!-- caller actor -->
|
||
<rect x="155" y="455" width="150" height="92" rx="7" fill="#13161e" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="230" y="475" fill="#e8eaf6" font-size="10" font-weight="700" text-anchor="middle">caller actor</text>
|
||
<text x="230" y="491" fill="#606880" font-size="8" text-anchor="middle">tx.send(Envelope::Call</text>
|
||
<text x="230" y="503" fill="#606880" font-size="8" text-anchor="middle">(req, reply_tx))</text>
|
||
<text x="230" y="517" fill="#606880" font-size="8" text-anchor="middle">reply_rx.recv() → parks</text>
|
||
<text x="230" y="531" fill="#606880" font-size="8" text-anchor="middle">recv_timeout = call_timeout</text>
|
||
|
||
<!-- inbox -->
|
||
<rect x="350" y="455" width="160" height="66" rx="7" fill="#13161e" stroke="#4ecdc4" stroke-width="1.5"/>
|
||
<text x="430" y="475" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">inbox channel</text>
|
||
<text x="430" y="491" fill="#606880" font-size="8" text-anchor="middle">Envelope::Call(req, reply_tx)</text>
|
||
<text x="430" y="503" fill="#606880" font-size="8" text-anchor="middle">| Envelope::Cast(msg)</text>
|
||
<text x="430" y="515" fill="#606880" font-size="8" text-anchor="middle">closed ⇒ graceful shutdown</text>
|
||
|
||
<!-- server loop -->
|
||
<rect x="560" y="440" width="230" height="124" rx="7" fill="#0d1220" stroke="#4ecdc4" stroke-width="1.5"/>
|
||
<text x="675" y="460" fill="#4ecdc4" font-size="11" font-weight="700" text-anchor="middle">server actor — server_loop</text>
|
||
<text x="675" y="478" fill="#c8d0e0" font-size="8" text-anchor="middle">select priority (rebuilt per turn):</text>
|
||
<text x="675" y="491" fill="#f5a623" font-size="8" text-anchor="middle">Down arms › Watcher arm › infos › inbox</text>
|
||
<text x="675" y="507" fill="#606880" font-size="8" text-anchor="middle">dispatch → handle_call / cast / info / down</text>
|
||
<text x="675" y="521" fill="#606880" font-size="8" text-anchor="middle">reply_tx.send(reply) — failed send is fine</text>
|
||
<text x="675" y="535" fill="#606880" font-size="8" text-anchor="middle">Terminate drop guard: terminate() on</text>
|
||
<text x="675" y="547" fill="#606880" font-size="8" text-anchor="middle">every exit (close · panic · stop)</text>
|
||
|
||
<!-- reply capsule -->
|
||
<rect x="350" y="555" width="160" height="44" rx="22" fill="#0d0f14" stroke="#56d364" stroke-width="1.5"/>
|
||
<text x="430" y="573" fill="#56d364" font-size="9" font-weight="700" text-anchor="middle">one-shot reply channel</text>
|
||
<text x="430" y="587" fill="#606880" font-size="8" text-anchor="middle">drop = ServerDown to caller</text>
|
||
|
||
<!-- out-of-band arms -->
|
||
<rect x="540" y="583" width="215" height="52" rx="7" fill="#13161e" stroke="#3a4060" stroke-width="1.5"/>
|
||
<text x="647" y="601" fill="#a8e6cf" font-size="9" font-weight="700" text-anchor="middle">out-of-band channels</text>
|
||
<text x="647" y="615" fill="#606880" font-size="8" text-anchor="middle">info Receivers (fixed at start) +</text>
|
||
<text x="647" y="627" fill="#606880" font-size="8" text-anchor="middle">Monitor rx, fed at runtime via Watcher</text>
|
||
|
||
<!-- plane B arrows -->
|
||
<path d="M 305 490 L 348 490" fill="none" stroke="#4ecdc4" stroke-width="1.2" marker-end="url(#gp-teal)"/>
|
||
<path d="M 510 490 L 558 490" fill="none" stroke="#4ecdc4" stroke-width="1.2" marker-end="url(#gp-teal)"/>
|
||
<path d="M 558 540 C 540 560 520 572 512 575" fill="none" stroke="#56d364" stroke-width="1.2" marker-end="url(#gp-green)"/>
|
||
<path d="M 348 577 C 300 577 270 560 240 549" fill="none" stroke="#56d364" stroke-width="1.2" marker-end="url(#gp-green)"/>
|
||
<text x="318" y="608" fill="#606880" font-size="7" text-anchor="middle">wakes the parked caller</text>
|
||
<path d="M 660 581 L 668 566" fill="none" stroke="#a8e6cf" stroke-width="1.2" marker-end="url(#gp-green)"/>
|
||
</svg>
|
||
</div>
|
||
|
||
<h3>The arm priority is the semantics</h3>
|
||
<p>Calls and casts travel the <strong>same</strong> inbox as <code>Envelope</code> variants — that's what makes them
|
||
mutually ordered, like Erlang. Everything out-of-band rides separate channels composed at the
|
||
wait: <code>select</code> priority is <strong>Down arms › Watcher arm › info channels (declaration order) ›
|
||
inbox</strong>, rebuilt each turn. A hot inbox can't starve a death notice or a system message;
|
||
conversely a hot info channel <em>can</em> starve the inbox — deliberately. A closed info arm is
|
||
silently dropped from the set; a closed <em>inbox</em> (every <code>ServerRef</code> gone) is graceful
|
||
shutdown.</p>
|
||
|
||
<h3>Death needs no monitor</h3>
|
||
<p>Server death detection falls out of channel closure. Already dead → the inbox is closed and
|
||
<code>send</code> fails → <code>ServerDown</code>. Dies mid-call (handler panic or <code>request_stop</code>) → the
|
||
reply sender drops as the server's stack unwinds → the parked caller's <code>recv</code> errors →
|
||
<code>ServerDown</code>. So <code>call_timeout</code> is exactly <code>recv_timeout</code> on the reply channel —
|
||
nothing registered, nothing to leak on the timeout path by construction. One Erlang-faithful
|
||
sharp edge: a timed-out request stays in the inbox and <strong>will still be handled</strong>; only the
|
||
reply is discarded. Design idempotent calls accordingly.</p>
|
||
|
||
<div class="callout">
|
||
<span class="callout-icon">📎</span>
|
||
<p><code>terminate()</code> is wired through a <strong>drop guard</strong>, so it runs on every exit path — clean close, panic, stop — not just the happy one. Keep it cheap and non-panicking: it may run mid-unwind, where a second panic aborts the process. The loop also calls <code>check!()</code> every iteration, so a server whose arms are never empty stays preemptible and cancellable.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- OTP -->
|
||
<section id="otp">
|
||
<div class="section-label">OTP — Failure As Data</div>
|
||
<h2>Monitors, Links, Supervisors, Registry</h2>
|
||
<p>All four are built on the same two primitives — the slot's cold lifecycle data, walked by
|
||
<code>finalize_actor</code>, and the epoch-matched unpark — and all four follow Erlang's semantics
|
||
closely enough that the BEAM chapter of your brain transfers.</p>
|
||
|
||
<div class="module-grid">
|
||
<div class="module-card">
|
||
<div class="module-name">monitor — watching without touching</div>
|
||
<p><code>monitor(pid)</code> returns a <code>Monitor</code>: a private <code>Receiver<Down></code> plus the identity for <code>demonitor</code>. Unidirectional, <strong>one-shot</strong> — exactly one <code>Down</code> ever; the channel closes after. <code>DownReason</code> is deliberately payload-free: a panic payload has one owner, the joiner. Monitoring an already-dead pid yields an immediate <code>NoProc</code> Down rather than silence.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">link — fate-sharing, opt-out via trap</div>
|
||
<p><code>link(pid)</code> is bidirectional and persistent. <em>Abnormal</em> death (panic or stop — never a normal return) propagates: a non-trapping peer is <code>request_stop</code>'d, and because its own links are walked when <em>it</em> finalizes, the stop cascades transitively — "let it crash." A peer that called <code>trap_exit()</code> instead receives an <code>ExitSignal</code> message and keeps running. Linking a dead pid behaves as an immediate <code>NoProc</code> death.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">supervisor — policy over mechanism</div>
|
||
<p>A supervisor is just an actor running <code>OneForOne::new().child(spec)…run()</code>. Strategies: <code>OneForOne</code> (restart the dead child), <code>OneForAll</code> (restart everything), <code>RestForOne</code> (restart it and everyone after it). <code>Restart::{Permanent, Transient, Temporary}</code> per child. The <strong>intensity cap</strong> (default 3 restarts per 5s, Erlang's default) stops a crash-looping child from burning the tree — tripping it kills the supervisor, escalating upward.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">registry — names with lazy hygiene</div>
|
||
<p><code>register(name, pid)</code> / <code>whereis(name)</code> / <code>name_of(pid)</code>: a bimap under one <code>RawMutex</code>, O(1) both ways. No hook in <code>finalize_actor</code> — every operation checks the bound pid's liveness via the generation-checked slot word, so a stale binding is <em>detectable, never misdirected</em>: dead bindings behave as absent and are pruned on contact. Zero coupling to the actor lifecycle, at the cost of a dead name lingering until touched.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<pre><code><span class="cm">// the whole OTP loop, in user space</span>
|
||
<span class="fn">spawn</span>(|| {
|
||
<span class="ty">OneForOne</span>::<span class="fn">new</span>()
|
||
.<span class="fn">strategy</span>(<span class="ty">Strategy</span>::RestForOne)
|
||
.<span class="fn">intensity</span>(<span class="nu">3</span>, <span class="ty">Duration</span>::<span class="fn">from_secs</span>(<span class="nu">5</span>))
|
||
.<span class="fn">child</span>(<span class="ty">ChildSpec</span>::<span class="fn">new</span>(<span class="ty">Restart</span>::Permanent, || <span class="fn">db_writer</span>()))
|
||
.<span class="fn">child</span>(<span class="ty">ChildSpec</span>::<span class="fn">new</span>(<span class="ty">Restart</span>::Transient, || <span class="fn">batch_job</span>()))
|
||
.<span class="fn">run</span>(); <span class="cm">// parks on its Signal channel; restarts per policy</span>
|
||
});</code></pre>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- GOTCHAS -->
|
||
<section id="gotchas">
|
||
<div class="section-label">Key Gotchas</div>
|
||
<h2>Things That Would Bite You</h2>
|
||
|
||
<div class="module-grid">
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--green)">Lost-wakeup window — closed</div>
|
||
<p class="good">The v0.3 doc's headline gotcha is now a theorem instead of a flag. An unpark racing the prep-to-park window lands as the <code>RunningNotified</code> <em>state</em>; the scheduler's park-return CAS re-queues instead of parking. Loom proves parked-forever unreachable across all interleavings — including the Blocking-IO path the old <code>pending_unpark</code> bool silently missed.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--green)">Global state mutex — gone</div>
|
||
<p class="good">The single <code>Mutex<SharedState></code> that capped scalability no longer exists. Slot lookup is lock-free against a fixed slab; the queue mutex (if <code>rq-mutex</code>) covers only push/pop; <code>rq-mpmc</code> / <code>rq-striped</code> remove even that. Timers and IO sit behind their own try-locked drain.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--red)"><code>std::thread::sleep</code> inside an actor</div>
|
||
<p>Still blocks the entire OS scheduler thread, starving every actor on it. There's no detection. Use <code>smarm::sleep(d)</code>. Same family: any blocking syscall outside <code>block_on_io</code> / <code>wait_readable</code>.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--red)">Panics in <code>terminate()</code></div>
|
||
<p>gen_server's <code>terminate()</code> runs from a drop guard, possibly mid-unwind. A panic inside it during an unwind is a double panic → process abort, no supervision tree to save you. Keep it cheap, non-blocking, non-panicking.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--yellow)">Cold locks are leaf locks</div>
|
||
<p>Per-slot <code>RawMutex</code> cold data follows a strict <strong>leaf rule: never hold two cold locks at once</strong> — <code>finalize_actor</code>'s link cascade locks peers one at a time. Overall order: <code>io</code> → (cold | free | stack_pool) → run queue innermost; timers independent. Internal code only, but if you're hacking on smarm, this is the map.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--yellow)">Timed-out calls are still handled</div>
|
||
<p><code>call_timeout</code> giving up does not cancel the request — it's already in the server's inbox and <strong>will run</strong>; only the reply is discarded (the abandoned reply channel makes the server's send fail harmlessly). Erlang behaves identically. Make calls idempotent or live with it.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--yellow)">Stale timer entries — now provably harmless</div>
|
||
<p>A granted lock or satisfied wait still leaves its timer entry in the heap (no cancellation). But the entry carries the wait's epoch, and the epoch was consumed by the real wake — so the late fire is a <em>proven</em> no-op, not a "should be fine." Cost unchanged: ~32 bytes per stale entry, bounded.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--yellow)">Stops are observation-point cancellation</div>
|
||
<p><code>request_stop</code> is cooperative: the sentinel is raised at the next allocation, <code>check!()</code>, or park wake. An actor spinning in a no-alloc loop without <code>check!()</code> is unstoppable, exactly as it is unpreemptible. Holding a <code>RawMutex</code> guard also defers the sentinel (by design — see Preemption).</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--green)">XMM registers not saved in context switch</div>
|
||
<p class="good">Intentional and correct. XMM0–15 are caller-saved in SysV AMD64 ABI. Every yield passes through a Rust call site, so the compiler has already spilled live XMM values to the actor's stack before the naked asm runs; they're restored from its own stack on resume.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--green)"><code>panic = unwind</code> is required</div>
|
||
<p class="good">The trampoline uses <code>catch_unwind</code> to intercept actor panics — and cooperative stops are <em>implemented as</em> an unwind (<code>StopSentinel</code>). With <code>panic = abort</code>, both become process death: no <code>Outcome::Panic</code>, no <code>Stopped</code>, no supervision. Documented; the profile is set in <code>Cargo.toml</code>.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
|
||
</body></html>
|