1297 lines
63 KiB
HTML
1297 lines
63 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="smarm%20-%20Deep%20Dive_files/css2.css" 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.3</span>
|
||
<a href="#overview">Overview</a>
|
||
<a href="#modules">Modules</a>
|
||
<a href="#deps">Dep Graph</a>
|
||
<a href="#init">Init</a>
|
||
<a href="#yield-cycle">Yield Cycle</a>
|
||
<a href="#spawn">Spawn</a>
|
||
<a href="#preempt">Preemption</a>
|
||
<a href="#io">IO</a>
|
||
<a href="#gotchas">Gotchas</a>
|
||
</nav>
|
||
|
||
<main>
|
||
|
||
<!-- HERO -->
|
||
<section class="hero" id="overview">
|
||
<div class="section-label">smarm — Silly Marks Abstract Rust Machine</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 prototype 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 single global run queue. 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.
|
||
</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. No copying GC. Message passing is a <code>move</code>, not a clone.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- MODULE MAP -->
|
||
<section id="modules">
|
||
<div class="section-label">Architecture</div>
|
||
<h2>Module Map</h2>
|
||
<p>13 source modules, three rough layers. The bottom layer has zero
|
||
smarm dependencies; middle layer builds the runtime machinery; top layer
|
||
is public API.</p>
|
||
|
||
<div class="diagram-wrap">
|
||
<svg width="900" height="420" viewBox="0 0 900 420" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;font-family:'JetBrains Mono',monospace">
|
||
<defs>
|
||
<marker id="arr" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L8,3 z" fill="#252a38"></path>
|
||
</marker>
|
||
<marker id="arr-acc" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L8,3 z" fill="#5b8af5"></path>
|
||
</marker>
|
||
</defs>
|
||
|
||
<!-- Layer labels -->
|
||
<text x="18" y="62" fill="#606880" font-size="9" letter-spacing="2" text-transform="uppercase">LAYER 0 — PRIMITIVES</text>
|
||
<text x="18" y="192" fill="#606880" font-size="9" letter-spacing="2">LAYER 1 — RUNTIME MACHINERY</text>
|
||
<text x="18" y="322" fill="#606880" font-size="9" letter-spacing="2">LAYER 2 — PUBLIC API / FACADE</text>
|
||
|
||
<!-- Layer 0 boxes -->
|
||
<!-- stack -->
|
||
<rect x="40" y="70" width="100" height="48" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="90" y="89" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">stack</text>
|
||
<text x="90" y="105" fill="#606880" font-size="9" text-anchor="middle">mmap + guard</text>
|
||
|
||
<!-- context -->
|
||
<rect x="160" y="70" width="100" height="48" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="210" y="89" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">context</text>
|
||
<text x="210" y="105" fill="#606880" font-size="9" text-anchor="middle">naked asm CSW</text>
|
||
|
||
<!-- preempt -->
|
||
<rect x="280" y="70" width="100" height="48" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="330" y="89" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">preempt</text>
|
||
<text x="330" y="105" fill="#606880" font-size="9" text-anchor="middle">alloc hook + RDTSC</text>
|
||
|
||
<!-- pid -->
|
||
<rect x="400" y="70" width="100" height="48" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="450" y="89" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">pid</text>
|
||
<text x="450" y="105" fill="#606880" font-size="9" text-anchor="middle">(index, gen) pair</text>
|
||
|
||
<!-- timer -->
|
||
<rect x="520" y="70" width="100" height="48" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="570" y="89" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">timer</text>
|
||
<text x="570" y="105" fill="#606880" font-size="9" text-anchor="middle">min-heap</text>
|
||
|
||
<!-- supervisor -->
|
||
<rect x="640" y="70" width="110" height="48" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="695" y="89" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">supervisor</text>
|
||
<text x="695" y="105" fill="#606880" font-size="9" text-anchor="middle">Signal enum only</text>
|
||
|
||
<!-- trace -->
|
||
<rect x="770" y="70" width="100" height="48" rx="6" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="820" y="89" fill="#5b8af5" font-size="11" font-weight="700" text-anchor="middle">trace</text>
|
||
<text x="820" y="105" fill="#606880" font-size="9" text-anchor="middle">Chrome JSON opt</text>
|
||
|
||
<!-- Layer 1 boxes -->
|
||
<!-- actor -->
|
||
<rect x="100" y="200" width="110" height="48" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="155" y="219" fill="#4ecdc4" font-size="11" font-weight="700" text-anchor="middle">actor</text>
|
||
<text x="155" y="235" fill="#606880" font-size="9" text-anchor="middle">trampoline + TLs</text>
|
||
|
||
<!-- io -->
|
||
<rect x="230" y="200" width="110" height="48" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="285" y="219" fill="#4ecdc4" font-size="11" font-weight="700" text-anchor="middle">io</text>
|
||
<text x="285" y="235" fill="#606880" font-size="9" text-anchor="middle">epoll + pool thread</text>
|
||
|
||
<!-- channel -->
|
||
<rect x="360" y="200" width="110" height="48" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="415" y="219" fill="#4ecdc4" font-size="11" font-weight="700" text-anchor="middle">channel</text>
|
||
<text x="415" y="235" fill="#606880" font-size="9" text-anchor="middle">MPSC, park/unpark</text>
|
||
|
||
<!-- mutex -->
|
||
<rect x="490" y="200" width="110" height="48" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="545" y="219" fill="#4ecdc4" font-size="11" font-weight="700" text-anchor="middle">mutex</text>
|
||
<text x="545" y="235" fill="#606880" font-size="9" text-anchor="middle">timeout + FIFO</text>
|
||
|
||
<!-- runtime -->
|
||
<rect x="620" y="200" width="110" height="48" rx="6" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="675" y="219" fill="#4ecdc4" font-size="11" font-weight="700" text-anchor="middle">runtime</text>
|
||
<text x="675" y="235" fill="#606880" font-size="9" text-anchor="middle">SharedState + loop</text>
|
||
|
||
<!-- Layer 2 -->
|
||
<!-- scheduler -->
|
||
<rect x="270" y="330" width="130" height="48" rx="6" fill="#0a0c12" stroke="#5b8af5" stroke-width="1.5"></rect>
|
||
<text x="335" y="349" fill="#f5a623" font-size="11" font-weight="700" text-anchor="middle">scheduler</text>
|
||
<text x="335" y="365" fill="#606880" font-size="9" text-anchor="middle">public API facade</text>
|
||
|
||
<!-- lib -->
|
||
<rect x="430" y="330" width="130" height="48" rx="6" fill="#0a0c12" stroke="#5b8af5" stroke-width="1.5"></rect>
|
||
<text x="495" y="349" fill="#f5a623" font-size="11" font-weight="700" text-anchor="middle">lib.rs</text>
|
||
<text x="495" y="365" fill="#606880" font-size="9" text-anchor="middle">re-exports + GlobalAlloc</text>
|
||
|
||
<!-- ARROWS: L0 → L1 (faint) -->
|
||
<!-- stack → actor -->
|
||
<line x1="115" y1="118" x2="130" y2="198" stroke="#252a38" stroke-width="1" marker-end="url(#arr)"></line>
|
||
<!-- context → actor -->
|
||
<line x1="210" y1="118" x2="170" y2="198" stroke="#252a38" stroke-width="1" marker-end="url(#arr)"></line>
|
||
<!-- context → runtime -->
|
||
<line x1="240" y1="118" x2="640" y2="198" stroke="#252a38" stroke-width="1" stroke-dasharray="3,3"></line>
|
||
<!-- preempt → runtime -->
|
||
<line x1="350" y1="118" x2="650" y2="198" stroke="#252a38" stroke-width="1" marker-end="url(#arr)"></line>
|
||
<!-- pid → actor -->
|
||
<line x1="430" y1="118" x2="200" y2="198" stroke="#252a38" stroke-width="1" stroke-dasharray="3,3"></line>
|
||
<!-- pid → runtime -->
|
||
<line x1="470" y1="118" x2="655" y2="198" stroke="#252a38" stroke-width="1" stroke-dasharray="3,3"></line>
|
||
<!-- timer → runtime -->
|
||
<line x1="570" y1="118" x2="685" y2="198" stroke="#252a38" stroke-width="1" marker-end="url(#arr)"></line>
|
||
<!-- timer → mutex -->
|
||
<line x1="545" y1="118" x2="530" y2="198" stroke="#252a38" stroke-width="1" marker-end="url(#arr)"></line>
|
||
<!-- supervisor → runtime (via channel) -->
|
||
<line x1="695" y1="118" x2="720" y2="198" stroke="#252a38" stroke-width="1" stroke-dasharray="3,3"></line>
|
||
|
||
<!-- ARROWS: L1 → L2 -->
|
||
<!-- runtime → scheduler -->
|
||
<line x1="645" y1="248" x2="390" y2="328" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#arr-acc)" stroke-dasharray="4,2"></line>
|
||
<!-- actor → scheduler -->
|
||
<line x1="185" y1="248" x2="295" y2="328" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#arr-acc)"></line>
|
||
<!-- channel → scheduler -->
|
||
<line x1="415" y1="248" x2="380" y2="328" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#arr-acc)"></line>
|
||
<!-- mutex → scheduler -->
|
||
<line x1="525" y1="248" x2="420" y2="328" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#arr-acc)" stroke-dasharray="4,2"></line>
|
||
<!-- runtime → lib -->
|
||
<line x1="700" y1="248" x2="540" y2="328" stroke="#5b8af5" stroke-width="1.5" marker-end="url(#arr-acc)" stroke-dasharray="4,2"></line>
|
||
|
||
<!-- Legend -->
|
||
<line x1="40" y1="400" x2="70" y2="400" stroke="#252a38" stroke-width="1.5"></line>
|
||
<text x="76" y="404" fill="#606880" font-size="9">uses directly</text>
|
||
<line x1="160" y1="400" x2="190" y2="400" stroke="#252a38" stroke-width="1.5" stroke-dasharray="3,3"></line>
|
||
<text x="196" y="404" fill="#606880" font-size="9">uses via type (Pid etc)</text>
|
||
<line x1="340" y1="400" x2="370" y2="400" stroke="#5b8af5" stroke-width="1.5"></line>
|
||
<text x="376" y="404" fill="#606880" font-size="9">public API edge</text>
|
||
</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>Calls <code>mmap</code> for a contiguous region, then <code>mprotect</code>'s the bottom page to <code>PROT_NONE</code>. Stack grows downward; overflow hits the guard page → SIGSEGV. Implements <code>Drop</code> via <code>munmap</code>. Zero smarm dependencies.</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 not
|
||
saved here — compiler guarantees spill 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> — wraps <code>System</code> allocator. On every Nth alloc, reads RDTSC. If elapsed > <code>timeslice_cycles</code> and preemption is enabled, calls <code>switch_to_scheduler()</code>. Thread-locals hold the countdown, start timestamp, and an enabled flag (scheduler disables it to prevent self-preemption).</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">pid</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p><code>struct Pid(u32 index, u32 generation)</code>. Index = slot in the actor table. Generation increments on actor death. Stale handles are detectable: a <code>Pid</code> with wrong generation fails slot lookup rather than silently addressing a new actor. Solves ABA without exhausting PID space.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">actor</div>
|
||
<div class="module-layer">Layer 1 · machinery</div>
|
||
<p>Owns the <code>Stack</code>. Defines the <code>trampoline</code>: every actor's first <code>ret</code> lands here. Trampoline reads the closure from a thread-local, calls it inside <code>catch_unwind</code>, writes the <code>Outcome</code>
|
||
to another thread-local, then yields back to the scheduler.
|
||
Thread-locals: current PID, pending closure, last outcome, done flag.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">runtime</div>
|
||
<div class="module-layer">Layer 1 · core</div>
|
||
<p>The heaviest module. Contains <code>SharedState</code> (slot table, run queue, timers, IO), <code>RuntimeInner</code> (shared state behind a mutex, per-thread stats, drain lock), and <code>schedule_loop</code>
|
||
— the main scheduler loop that drains timers, drains IO completions,
|
||
pops actors, resumes them, and handles the post-yield intent (re-queue
|
||
vs park vs finalize).</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">channel</div>
|
||
<div class="module-layer">Layer 1 · primitive</div>
|
||
<p>Unbounded MPSC. Inner state is <code>Arc<Mutex<Inner<T>>></code> — senders are clonable, last drop closes channel. <code>recv()</code>: checks queue; if empty, registers self as <code>parked_receiver</code>, releases the lock, calls <code>park_current()</code>. <code>send()</code>: pushes, takes the parked PID, calls <code>unpark(pid)</code>.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">mutex</div>
|
||
<div class="module-layer">Layer 1 · primitive</div>
|
||
<p>Actor-aware mutex with mandatory timeout (default 30s). Fast
|
||
path: no holder → grant immediately. Slow path: join FIFO waiter queue,
|
||
insert a <code>WaitTimeout</code> timer, park. On timer expiry: if actor is still in waiters, unpark it with <code>LockTimeout</code>. On guard drop: pop next waiter, grant, unpark.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">io</div>
|
||
<div class="module-layer">Layer 1 · machinery</div>
|
||
<p>Two background OS threads: an <strong>epoll thread</strong> (waits on fds with EPOLLONESHOT; on ready, pushes <code>FdReady</code> completion) and a <strong>pool thread</strong> (runs blocking closures inside <code>catch_unwind</code>; pushes <code>Blocking</code> completion). Both write a wake pipe byte to stir the scheduler. Completions are drained inside <code>schedule_loop</code>.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">timer</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p><code>BinaryHeap<Reverse<Entry>></code> = min-heap by deadline. Two <code>Reason</code> variants: <code>Sleep</code> (unpark unconditionally) and <code>WaitTimeout</code> (call <code>target.on_timeout()</code>). No cancellation — stale entries are no-ops on pop. Entries inserted by <code>sleep()</code> and <code>mutex::lock_timeout()</code>.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">scheduler</div>
|
||
<div class="module-layer">Layer 2 · public facade</div>
|
||
<p>Thin facade. Exposes <code>spawn</code>, <code>yield_now</code>, <code>park_current</code>, <code>unpark</code>, <code>sleep</code>, <code>block_on_io</code>, <code>wait_readable</code>, <code>wait_writable</code>, <code>run</code>. All delegate to <code>runtime</code>. Also owns <code>JoinHandle</code> and the <code>NoPreempt</code> RAII guard.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name">supervisor</div>
|
||
<div class="module-layer">Layer 0 · primitive</div>
|
||
<p>Just the <code>Signal</code> enum: <code>Exit(Pid)</code> or <code>Panic(Pid, Box<dyn Any+Send>)</code>. No restart logic — that's user-space policy. Signals are delivered via the supervisor actor's own channel (<code>Sender<Signal></code> stored in the child's slot).</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 critical insight: <code>runtime.rs</code> is the hub. Every substantive module either feeds into it or is orchestrated by it. <code>scheduler.rs</code> is purely a facade — it imports <code>runtime</code> and re-exports it through the public API.</p>
|
||
|
||
<div class="diagram-wrap">
|
||
<svg width="860" height="320" viewBox="0 0 860 320" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;font-family:'JetBrains Mono',monospace">
|
||
<defs>
|
||
<marker id="a2" markerWidth="7" markerHeight="7" refX="5" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L7,3 z" fill="#5b8af5"></path>
|
||
</marker>
|
||
</defs>
|
||
|
||
<!-- runtime center -->
|
||
<rect x="340" y="120" width="140" height="52" rx="8" fill="#0d1220" stroke="#5b8af5" stroke-width="2"></rect>
|
||
<text x="410" y="141" fill="#5b8af5" font-size="12" font-weight="700" text-anchor="middle">runtime.rs</text>
|
||
<text x="410" y="157" fill="#606880" font-size="9" text-anchor="middle">SharedState · schedule_loop</text>
|
||
|
||
<!-- Feeding into runtime: stack -->
|
||
<rect x="30" y="20" width="90" height="38" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="75" y="38" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">stack</text>
|
||
<text x="75" y="51" fill="#606880" font-size="8" text-anchor="middle">Stack::new()</text>
|
||
<line x1="120" y1="39" x2="340" y2="135" stroke="#5b8af5" stroke-width="1" marker-end="url(#a2)"></line>
|
||
|
||
<!-- context -->
|
||
<rect x="140" y="20" width="90" height="38" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="185" y="38" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">context</text>
|
||
<text x="185" y="51" fill="#606880" font-size="8" text-anchor="middle">switch fns</text>
|
||
<line x1="215" y1="39" x2="355" y2="120" stroke="#5b8af5" stroke-width="1" marker-end="url(#a2)"></line>
|
||
|
||
<!-- preempt -->
|
||
<rect x="250" y="20" width="90" height="38" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="295" y="38" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">preempt</text>
|
||
<text x="295" y="51" fill="#606880" font-size="8" text-anchor="middle">RDTSC + hook</text>
|
||
<line x1="310" y1="58" x2="376" y2="120" stroke="#5b8af5" stroke-width="1" marker-end="url(#a2)"></line>
|
||
|
||
<!-- actor -->
|
||
<rect x="360" y="20" width="90" height="38" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="405" y="38" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">actor</text>
|
||
<text x="405" y="51" fill="#606880" font-size="8" text-anchor="middle">trampoline</text>
|
||
<line x1="405" y1="58" x2="405" y2="120" stroke="#5b8af5" stroke-width="1" marker-end="url(#a2)"></line>
|
||
|
||
<!-- timer -->
|
||
<rect x="470" y="20" width="90" height="38" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="515" y="38" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">timer</text>
|
||
<text x="515" y="51" fill="#606880" font-size="8" text-anchor="middle">min-heap</text>
|
||
<line x1="510" y1="58" x2="450" y2="120" stroke="#5b8af5" stroke-width="1" marker-end="url(#a2)"></line>
|
||
|
||
<!-- io -->
|
||
<rect x="575" y="20" width="90" height="38" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="620" y="38" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">io</text>
|
||
<text x="620" y="51" fill="#606880" font-size="8" text-anchor="middle">epoll + pool</text>
|
||
<line x1="600" y1="58" x2="480" y2="120" stroke="#5b8af5" stroke-width="1" marker-end="url(#a2)"></line>
|
||
|
||
<!-- supervisor -->
|
||
<rect x="680" y="20" width="100" height="38" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="730" y="38" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">supervisor</text>
|
||
<text x="730" y="51" fill="#606880" font-size="8" text-anchor="middle">Signal enum</text>
|
||
<line x1="710" y1="58" x2="480" y2="135" stroke="#5b8af5" stroke-width="1" marker-end="url(#a2)"></line>
|
||
|
||
<!-- channel + mutex → runtime (through scheduler) -->
|
||
<rect x="140" y="220" width="100" height="38" rx="5" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="190" y="238" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">channel</text>
|
||
<text x="190" y="251" fill="#606880" font-size="8" text-anchor="middle">calls unpark()</text>
|
||
<!-- channel → runtime -->
|
||
<line x1="240" y1="240" x2="340" y2="172" stroke="#5b8af5" stroke-width="1" marker-end="url(#a2)" stroke-dasharray="4,2"></line>
|
||
|
||
<rect x="560" y="220" width="100" height="38" rx="5" fill="#1a1e2a" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="610" y="238" fill="#4ecdc4" font-size="10" font-weight="700" text-anchor="middle">mutex</text>
|
||
<text x="610" y="251" fill="#606880" font-size="8" text-anchor="middle">calls unpark()</text>
|
||
<!-- mutex → runtime -->
|
||
<line x1="565" y1="240" x2="480" y2="172" stroke="#5b8af5" stroke-width="1" marker-end="url(#a2)" stroke-dasharray="4,2"></line>
|
||
|
||
<!-- scheduler facade at bottom -->
|
||
<rect x="300" y="260" width="260" height="48" rx="8" fill="#0a0c12" stroke="#f5a623" stroke-width="2"></rect>
|
||
<text x="430" y="279" fill="#f5a623" font-size="12" font-weight="700" text-anchor="middle">scheduler.rs / lib.rs</text>
|
||
<text x="430" y="295" fill="#606880" font-size="9" text-anchor="middle">public API re-exports · GlobalAlloc</text>
|
||
<!-- scheduler ← runtime -->
|
||
<line x1="410" y1="172" x2="390" y2="260" stroke="#f5a623" stroke-width="1.5" marker-end="url(#a2)" stroke-dasharray="5,2"></line>
|
||
|
||
<!-- Mutual: runtime calls channel/mutex unpark via scheduler -->
|
||
<text x="430" y="210" fill="#606880" font-size="8" text-anchor="middle">runtime calls unpark() via scheduler</text>
|
||
<text x="430" y="220" fill="#606880" font-size="8" text-anchor="middle">channel/mutex call unpark() directly</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<div class="callout">
|
||
<span class="callout-icon">⚠</span>
|
||
<p><strong>Circular dependency:</strong> <code>channel</code> and <code>mutex</code> call <code>scheduler::unpark()</code>, which calls into <code>runtime</code>. And <code>runtime</code>'s <code>schedule_loop</code> resumes actors that run channel/mutex code. This is intentional — it's the cooperative unpark mechanism. It works because <code>unpark()</code> never blocks and preemption is disabled while holding any smarm internal lock.</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>Start <code>IoThread</code> <span class="tag">io.rs</span></h4>
|
||
<p>Creates a wake pipe (non-blocking <code>O_NONBLOCK</code>). Creates an <code>epollfd</code>. Creates a shutdown pipe and registers it 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">3</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">4</div>
|
||
<div class="flow-body">
|
||
<h4>Spawn initial actor <span class="tag">scheduler.rs</span></h4>
|
||
<p>Calls <code>scheduler::spawn(f)</code>. This locks <code>SharedState</code>, allocates a slot, creates a <code>Stack</code> via <code>mmap</code>, calls <code>init_actor_stack()</code> to write the initial register frame (trampoline address + 6 zero GPR slots), stores the closure in <code>pending_closures</code>, pushes the PID to the run queue, returns a <code>JoinHandle</code>.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">5</div>
|
||
<div class="flow-body">
|
||
<h4>Spawn N-1 OS scheduler threads</h4>
|
||
<p>For each extra thread: clone <code>Arc<RuntimeInner></code>, spawn OS thread, set <code>RUNTIME</code> and <code>SCHED_SLOT</code> thread-locals, enter <code>schedule_loop</code>. Thread 0 is the calling thread.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">6</div>
|
||
<div class="flow-body">
|
||
<h4>Enter <code>schedule_loop</code> on thread 0 <span class="tag">runtime.rs</span></h4>
|
||
<p>This is a <code>loop { drain → pop → resume → handle-intent }</code>.
|
||
Thread 0 blocks here until the run queue is empty and no timers or IO
|
||
are pending. All actors run inside this loop. This call does not return
|
||
until the program is done.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">7</div>
|
||
<div class="flow-body">
|
||
<h4>Shutdown sequence</h4>
|
||
<p>All scheduler threads return from <code>schedule_loop</code>. OS threads are joined. <code>IoThread::drop()</code> is called: writes shutdown pipe → epoll thread exits; drops the mpsc sender → pool thread exits; closes all fds. <code>SharedState</code> is cleared for potential next <code>run()</code> call.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- YIELD CYCLE -->
|
||
<section id="yield-cycle">
|
||
<div class="section-label">Core Mechanism</div>
|
||
<h2>The Yield → Schedule → Resume Cycle</h2>
|
||
<p>This is the heartbeat of the entire runtime. Every context switch
|
||
follows exactly this path, whether triggered by a cooperative yield,
|
||
preemption, channel recv, mutex contention, or IO wait.</p>
|
||
|
||
<div class="diagram-wrap">
|
||
<svg width="820" height="480" viewBox="0 0 820 480" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;font-family:'JetBrains Mono',monospace">
|
||
<defs>
|
||
<marker id="ab" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L8,3 z" fill="#f5a623"></path>
|
||
</marker>
|
||
<marker id="ab2" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L8,3 z" fill="#4ecdc4"></path>
|
||
</marker>
|
||
</defs>
|
||
|
||
<!-- ACTOR column -->
|
||
<text x="130" y="28" fill="#606880" font-size="9" text-anchor="middle" letter-spacing="2">ACTOR STACK</text>
|
||
<line x1="130" y1="35" x2="130" y2="450" stroke="#252a38" stroke-width="1" stroke-dasharray="4,3"></line>
|
||
|
||
<!-- SCHEDULER column -->
|
||
<text x="430" y="28" fill="#606880" font-size="9" text-anchor="middle" letter-spacing="2">SCHEDULER OS THREAD</text>
|
||
<line x1="430" y1="35" x2="430" y2="450" stroke="#252a38" stroke-width="1" stroke-dasharray="4,3"></line>
|
||
|
||
<!-- RUNTIME column -->
|
||
<text x="700" y="28" fill="#606880" font-size="9" text-anchor="middle" letter-spacing="2">SHARED STATE</text>
|
||
<line x1="700" y1="35" x2="700" y2="450" stroke="#252a38" stroke-width="1" stroke-dasharray="4,3"></line>
|
||
|
||
<!-- Step A: Actor running -->
|
||
<rect x="50" y="45" width="160" height="38" rx="5" fill="#13161e" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="130" y="62" fill="#a8e6cf" font-size="10" font-weight="500" text-anchor="middle">actor code running</text>
|
||
<text x="130" y="75" fill="#606880" font-size="8" text-anchor="middle">PREEMPTION_ENABLED = true</text>
|
||
|
||
<!-- Step B: yield triggered -->
|
||
<rect x="50" y="110" width="160" height="38" rx="5" fill="#1a1e2a" stroke="#f5a623" stroke-width="1.5"></rect>
|
||
<text x="130" y="127" fill="#f5a623" font-size="10" font-weight="500" text-anchor="middle">yield triggered</text>
|
||
<text x="130" y="140" fill="#606880" font-size="8" text-anchor="middle">set YieldIntent, call switch_to_sched()</text>
|
||
|
||
<line x1="130" y1="83" x2="130" y2="110" stroke="#f5a623" stroke-width="1" marker-end="url(#ab)"></line>
|
||
|
||
<!-- Step C: assembly runs -->
|
||
<rect x="50" y="175" width="160" height="50" rx="5" fill="#0a0c12" stroke="#252a38" stroke-width="1"></rect>
|
||
<text x="130" y="192" fill="#bc8cff" font-size="9" font-weight="700" text-anchor="middle">x86-64 naked asm</text>
|
||
<text x="130" y="205" fill="#606880" font-size="8" text-anchor="middle">push rbx,rbp,r12-r15</text>
|
||
<text x="130" y="216" fill="#606880" font-size="8" text-anchor="middle">save actor rsp → ACTOR_SP TL</text>
|
||
|
||
<line x1="130" y1="148" x2="130" y2="175" stroke="#606880" stroke-width="1" marker-end="url(#ab)"></line>
|
||
|
||
<!-- Arrow: actor rsp saved, load sched rsp -->
|
||
<line x1="210" y1="200" x2="350" y2="200" stroke="#f5a623" stroke-width="1.5" marker-end="url(#ab)"></line>
|
||
<text x="280" y="194" fill="#606880" font-size="8" text-anchor="middle">rsp swap</text>
|
||
|
||
<!-- Scheduler resumes from switch_to_actor() call -->
|
||
<rect x="350" y="175" width="160" height="50" rx="5" fill="#0a0c12" stroke="#252a38" stroke-width="1"></rect>
|
||
<text x="430" y="192" fill="#bc8cff" font-size="9" font-weight="700" text-anchor="middle">scheduler resumes</text>
|
||
<text x="430" y="205" fill="#606880" font-size="8" text-anchor="middle">pop rbx,rbp,r12-r15</text>
|
||
<text x="430" y="216" fill="#606880" font-size="8" text-anchor="middle">ret → back in schedule_loop()</text>
|
||
|
||
<!-- Post-yield handling -->
|
||
<rect x="350" y="250" width="160" height="60" rx="5" fill="#13161e" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="430" y="267" fill="#4ecdc4" font-size="10" font-weight="500" text-anchor="middle">post-yield handling</text>
|
||
<text x="430" y="280" fill="#606880" font-size="8" text-anchor="middle">PREEMPTION_ENABLED = false</text>
|
||
<text x="430" y="292" fill="#606880" font-size="8" text-anchor="middle">check is_actor_done()</text>
|
||
<text x="430" y="303" fill="#606880" font-size="8" text-anchor="middle">read YieldIntent</text>
|
||
|
||
<line x1="430" y1="225" x2="430" y2="250" stroke="#4ecdc4" stroke-width="1" marker-end="url(#ab2)"></line>
|
||
|
||
<!-- update SharedState -->
|
||
<line x1="510" y1="280" x2="640" y2="280" stroke="#4ecdc4" stroke-width="1" marker-end="url(#ab2)"></line>
|
||
<text x="575" y="274" fill="#606880" font-size="8" text-anchor="middle">lock shared</text>
|
||
|
||
<rect x="640" y="255" width="130" height="60" rx="5" fill="#13161e" stroke="#252a38" stroke-width="1.5"></rect>
|
||
<text x="705" y="272" fill="#a8e6cf" font-size="9" text-anchor="middle">save actor.sp</text>
|
||
<text x="705" y="285" fill="#a8e6cf" font-size="9" text-anchor="middle">if Yield: push run_queue</text>
|
||
<text x="705" y="298" fill="#a8e6cf" font-size="9" text-anchor="middle">if Park: state=Parked</text>
|
||
<text x="705" y="311" fill="#a8e6cf" font-size="9" text-anchor="middle">if Done: finalize_actor</text>
|
||
|
||
<!-- Next loop iteration: pop actor -->
|
||
<rect x="350" y="360" width="160" height="48" rx="5" fill="#13161e" stroke="#3a4060" stroke-width="1.5"></rect>
|
||
<text x="430" y="377" fill="#4ecdc4" font-size="10" font-weight="500" text-anchor="middle">pop next actor</text>
|
||
<text x="430" y="390" fill="#606880" font-size="8" text-anchor="middle">drain timers+IO first</text>
|
||
<text x="430" y="400" fill="#606880" font-size="8" text-anchor="middle">run_queue.pop_front()</text>
|
||
|
||
<line x1="430" y1="315" x2="430" y2="360" stroke="#4ecdc4" stroke-width="1" marker-end="url(#ab2)"></line>
|
||
|
||
<!-- Resume: set TLs, call switch_to_actor -->
|
||
<rect x="350" y="430" width="160" height="38" rx="5" fill="#1a1e2a" stroke="#56d364" stroke-width="1.5"></rect>
|
||
<text x="430" y="447" fill="#56d364" font-size="10" font-weight="500" text-anchor="middle">resume actor</text>
|
||
<text x="430" y="460" fill="#606880" font-size="8" text-anchor="middle">set TLs → switch_to_actor()</text>
|
||
|
||
<line x1="430" y1="408" x2="430" y2="430" stroke="#56d364" stroke-width="1" marker-end="url(#ab2)"></line>
|
||
|
||
<!-- actor back -->
|
||
<line x1="350" y1="449" x2="210" y2="449" stroke="#56d364" stroke-width="1.5" marker-end="url(#ab2)"></line>
|
||
<text x="280" y="443" fill="#606880" font-size="8" text-anchor="middle">rsp swap</text>
|
||
<rect x="50" y="430" width="160" height="38" rx="5" fill="#13161e" stroke="#56d364" stroke-width="1.5"></rect>
|
||
<text x="130" y="447" fill="#56d364" font-size="10" font-weight="500" text-anchor="middle">actor resumes</text>
|
||
<text x="130" y="460" fill="#606880" font-size="8" text-anchor="middle">exactly where it yielded</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<h3>The 6 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>Actor stays Runnable; pushed back 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> triggers <code>switch_to_scheduler()</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>channel::recv()</code> (empty)</td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td><code>channel::send()</code> → <code>unpark()</code></td>
|
||
<td>Receiver PID stored in channel's <code>parked_receiver</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>mutex::lock()</code> (contended)</td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td><code>MutexGuard::drop()</code> or timer timeout</td>
|
||
<td>FIFO waiter queue; timeout via <code>WaitTimeout</code> timer entry</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>sleep(d)</code></td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td>Timer heap → <code>schedule_loop</code> drain</td>
|
||
<td>Inserts <code>Reason::Sleep</code> entry; scheduler unparks on pop</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>wait_readable/writable(fd)</code></td>
|
||
<td><span class="pill pill-yellow">Park</span></td>
|
||
<td>epoll thread → completion queue → scheduler</td>
|
||
<td>EPOLLONESHOT; one ADD → one wakeup → one DEL per call</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</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 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>Allocates a slot from free list or grows the slots vec. Assigns <code>Pid(index, generation)</code>. Creates a <code>Stack</code> (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> function pointer as the <code>ret</code> address, then 6 zero words for the callee-saved registers. The resulting <code>rsp</code> is stored as <code>actor.sp</code>. 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 separately</h4>
|
||
<p>The closure <code>Box<dyn FnOnce() + Send></code> goes into <code>SharedState::pending_closures</code> keyed by PID — <em>not</em>
|
||
on the actor's stack. This is because we can't pass it via a register
|
||
during first resume. The PID is pushed to the run queue; slot state is <code>Runnable</code>.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">4</div>
|
||
<div class="flow-body">
|
||
<h4>Scheduler picks up the PID, prepares first resume</h4>
|
||
<p>Before calling <code>switch_to_actor()</code>, the scheduler pops the closure from <code>pending_closures</code> and writes it to the <code>CURRENT_ACTOR_BOX</code> thread-local. Then sets <code>ACTOR_SP</code>, sets <code>CURRENT_PID</code>, arms the timeslice, enables preemption.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">5</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 <code>actor.sp</code> as the new <code>rsp</code>, pops the 6 zero words (restoring the "saved" registers to zero), then <code>ret</code>s — which pops the trampoline address from the stack and jumps to it. We're now executing on the actor's stack.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">6</div>
|
||
<div class="flow-body">
|
||
<h4><code>trampoline()</code> reads the closure and runs it</h4>
|
||
<p>Takes the closure from <code>CURRENT_ACTOR_BOX</code> thread-local (consuming it — subsequent resumes skip this). Calls it inside <code>panic::catch_unwind(AssertUnwindSafe(f))</code>. The actor's code runs normally from here. Any yield (channel, mutex, preemption) calls <code>switch_to_scheduler()</code>; the scheduler saves actor state, processes intent, loops.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flow-step">
|
||
<div class="flow-num">7</div>
|
||
<div class="flow-body">
|
||
<h4>Actor returns → trampoline handles completion</h4>
|
||
<p>If <code>catch_unwind</code> returns <code>Ok(())</code>, outcome is <code>Exit</code>. If it returns <code>Err(payload)</code>, outcome is <code>Panic(payload)</code>. Either way, outcome is written to <code>LAST_OUTCOME</code> thread-local, <code>ACTOR_DONE</code> is set to true, then <code>switch_to_scheduler()</code> is called for the last time. Scheduler sees <code>is_actor_done() == true</code>, calls <code>finalize_actor()</code>: delivers <code>Signal</code> to supervisor, unparks joiners, reclaims slot.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<!-- PREEMPTION -->
|
||
<section id="preempt">
|
||
<div class="section-label">Preemption</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> function — for tight loops that make no allocations.</p>
|
||
</div>
|
||
<div>
|
||
<h3>Invariant: preemption must be off when holding smarm locks</h3>
|
||
<p>If preemption fired while the scheduler held <code>SharedState</code>, the context switch would try to re-acquire the same mutex → deadlock. smarm prevents this with:</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;"><code>with_shared()</code> saves and disables preemption while the mutex is held</li>
|
||
<li style="margin-bottom:0.4rem;"><code>NoPreempt</code> RAII guard used in channel/mutex slow paths</li>
|
||
<li><code>trace::record()</code> also disables preemption (it can allocate)</li>
|
||
</ul>
|
||
<div class="callout" style="margin-top:0">
|
||
<span class="callout-icon">⚠</span>
|
||
<p class="warn">Known gap: tight no-alloc loops are invisible without explicit <code>check!()</code> calls. This is 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="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">→ park_current()</text>
|
||
<text x="80" y="190" fill="#ff6b6b" font-size="8" text-anchor="middle">→ state = Parked</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, 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">lookup waiters[fd]</text>
|
||
<text x="740" y="177" fill="#606880" font-size="8" text-anchor="middle">unpark(pid)</text>
|
||
<text x="740" y="192" fill="#606880" font-size="8" text-anchor="middle">Blocking →</text>
|
||
<text x="740" y="204" fill="#606880" font-size="8" text-anchor="middle">store in slot, unpark</text>
|
||
|
||
<!-- Arrows -->
|
||
<!-- actor → epoll (epoll_ctl via scheduler) -->
|
||
<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>
|
||
|
||
<!-- actor → pool (via submit) -->
|
||
<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>
|
||
|
||
<!-- epoll → completions -->
|
||
<line x1="380" y1="80" x2="460" y2="130" stroke="#f5a623" stroke-width="1" marker-end="url(#ai2)"></line>
|
||
<!-- pool → completions -->
|
||
<line x1="380" y1="220" x2="460" y2="170" stroke="#f5a623" stroke-width="1" marker-end="url(#ai2)"></line>
|
||
|
||
<!-- completions → scheduler -->
|
||
<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>
|
||
|
||
<!-- wake pipe (both threads → scheduler) -->
|
||
<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>
|
||
|
||
<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 — this is legal per the <code>epoll_ctl(2)</code> man page even while the epoll thread is inside <code>epoll_wait</code>. Avoids needing a second command channel.</p>
|
||
</div>
|
||
</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(--red)">Lost-wakeup window</div>
|
||
<p>Between registering as a channel's <code>parked_receiver</code> and calling <code>park_current()</code>, a sender could call <code>unpark()</code>. At that moment the actor is still <code>Runnable</code>, so <code>unpark()</code> sets <code>pending_unpark = true</code> instead of re-queuing. The scheduler checks this flag after the <code>Park</code> yield and re-queues immediately rather than parking. This flag also protects epoll and mutex paths.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--red)"><code>std::thread::sleep</code> inside actor</div>
|
||
<p>Blocks the entire OS scheduler thread, starving every actor assigned to that thread. There's no detection. Use <code>smarm::sleep(d)</code> instead.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--red)">Allocations while holding <code>SharedState</code></div>
|
||
<p>The <code>with_shared()</code> helper disables preemption while the mutex is held. But any code path that allocates inside <code>with_shared</code> <em>and</em> then tries to acquire <code>SharedState</code> again will deadlock. All internal smarm code is carefully structured to avoid this.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--yellow)">Global run queue mutex</div>
|
||
<p>All N scheduler threads contend on a single <code>Mutex<SharedState></code>.
|
||
This is the primary scalability ceiling — visible in the benchmark
|
||
suite as "tokio-favored" scenarios. Identified, documented, deferred.
|
||
The fix would be per-thread deques with work stealing.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--yellow)">No timer cancellation</div>
|
||
<p>When a mutex lock is granted before its timeout, the timer
|
||
entry stays in the heap. It fires eventually, the callback sees "actor
|
||
is no longer waiting" and no-ops. Cost is ~32 bytes and a few cycles per
|
||
stale entry. Bounded by one entry per parked actor.</p>
|
||
</div>
|
||
<div class="module-card">
|
||
<div class="module-name" style="color:var(--yellow)">fd leak on actor death during IO wait</div>
|
||
<p>If an actor dies while waiting on an fd, the epoll registration
|
||
is leaked. EPOLLONESHOT bounds damage to one stale wakeup, which the
|
||
scheduler drops when it can't find the PID in <code>waiters</code>. Noted in <code>io.rs</code> as a known gap for a future pass.</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">This is 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 we get to the naked asm. They're restored when the actor
|
||
resumes because they're on its own stack.</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 before they reach the naked assembly shim. If a user sets <code>panic = abort</code>,
|
||
panics kill the process instead of being caught — the supervision tree
|
||
collapses to process death. This is documented and the profile is set in
|
||
<code>Cargo.toml</code>.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
|
||
</body></html> |