Update the documentation

This commit is contained in:
smarm
2026-05-25 22:14:07 +02:00
parent 2b85ef60b2
commit d432349f99
6 changed files with 1348 additions and 25 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
target
Cargo.lock
smarm_trace.json

View File

@@ -1,8 +1,8 @@
# smarm
> Silly Marks Abstract Rust Machine. A prototype green-thread actor runtime for Rust.
> SMARM — Smarm, Marks Actor Runtime Machinery. A proof-of-concept green-thread actor runtime for Rust.
Implements the core ideas in [`LOOM.md`](./LOOM.md): green-thread actors on a
Implements the core ideas in [`Achitecture.md`](.docs/Architecture.md): green-thread actors on a
shared heap, scheduled cooperatively, communicating only by `Send` messages.
Erlang's isolation model without Erlang's copying GC, Rust's zero-copy
ownership transfers without async's function colouring.
@@ -58,7 +58,6 @@ tests/
per-module integration tests
benches/
primes.rs fan-out/fan-in compute, vs tokio current_thread
LOOM.md design intent
```
## Building and running
@@ -76,7 +75,26 @@ cargo bench # primes benchmark vs tokio
## What's not here
See the **Defer** section of `LOOM.md`. Notable absences: supervisor
See the **Defer** section of `Architecture.md`.
restart-intensity caps, `join!` for handle groups, stack growth via remap,
hierarchical timer wheel, fd-wait timeouts, `Signal::Timeout`. Each is
mechanism we know how to add; none belongs in this iteration.
## Docs
| Document | What it covers |
|---|---|
| [`Architecture.md`](./docs/Architecture.md) | Design intent, runtime model, and deferred work |
| [`smarm - Deep Dive.html`](./docs/smarm%20-%20Deep%20Dive.html) | Generated walkthrough of the system; good starting point |
| [`BENCHMARKS_AND_TUNING.md`](./docs/BENCHMARKS_AND_TUNING.md) | Where smarm wins and loses vs tokio, preemption knob recommendations |
| [`benchmarks.md`](./docs/benchmarks.md) | Raw benchmark results, methodology, and tuning experiment log |
## Contributing
This is a personal proof-of-concept. There's no PR workflow — if you fork it
and do something interesting, just send me an email. I'd genuinely like to
hear about it.
---
<sub>The name is a recursive acronym. The M is for Marks, as in the BEAM — Bogdan/Björn's Erlang Abstract Machine, the virtual machine that runs Erlang and Elixir. smarm is not the BEAM. It just admires it from a safe distance.</sub>

View File

@@ -1,4 +1,4 @@
# Loom
# SMARM Architecture
> Erlang-style actor concurrency for Rust, without the copies, the colors, or the GC pauses.
@@ -11,7 +11,7 @@ draws the boundary, the borrow checker already enforces it. What it lacks is an
async/await is IO-centric, colors your functions, and trades stack simplicity for state-machine complexity;
OS threads are too heavy to spawn per actor.
Loom adds a third option: **green-thread actors on a shared heap**, scheduled cooperatively, with
SMARM adds a third option: **green-thread actors on a shared heap**, scheduled cooperatively, with
message-passing as the only cross-actor communication primitive. You get Erlang's isolation model without
Erlang's copying GC, and you get Rust's zero-copy ownership transfers without async's cognitive overhead.
No function coloring. No `Box<dyn Future>`. Just actors, messages, and the borrow checker doing what it
@@ -24,14 +24,14 @@ already does.
### Actors and scheduling
Each actor is a lightweight green thread with its own heap-allocated, growable stack. Stacks are
allocated via `mmap` with a guard page below the region; overflow is detected by the OS without Loom
allocated via `mmap` with a guard page below the region; overflow is detected by the OS without SMARM
polling for it. Initial stacks are small and grow by remapping on demand.
The scheduler runs one OS thread per CPU. Each scheduler thread loops against a single global
`Mutex<HashMap>` queue shared across all schedulers. If queue contention becomes a measured bottleneck
this can be revisited; the interface will not change.
Loom requires `panic = unwind`. Users who set `panic = abort` accept that supervision and actor
SMARM requires `panic = unwind`. Users who set `panic = abort` accept that supervision and actor
isolation are silently degraded to process death.
### Process descriptor
@@ -84,11 +84,11 @@ threshold is exceeded the actor yields. The workloads that starve a scheduler
data transformation — are precisely the ones doing frequent allocations, so this approximation is
correct by construction.
`RDTSC` is not monotonic across core migration; a slightly wrong timeslice is acceptable. Loom is
`RDTSC` is not monotonic across core migration; a slightly wrong timeslice is acceptable. SMARM is
not a real-time scheduler.
Known failure mode: tight no-alloc loops are invisible to this mechanism. Actors doing sustained
allocation-free compute must call `loom::yield_now()` explicitly, or offload to a thread pool
allocation-free compute must call `smarm::yield_now()` explicitly, or offload to a thread pool
outside the actor scheduler (e.g. rayon). This is documented and acceptable — such loops are rare
in message-passing workloads.
@@ -99,12 +99,12 @@ An actor yields at:
- **Channel send/recv** — the primary communication primitive
- **Mutex contention** — attempting to lock a held `Arc<Mutex<>>` parks the actor
- **IO** — blocking on a socket or file descriptor parks the actor until the IO thread signals readiness
- **`loom::sleep(duration)`** — parks the actor; the timer wheel re-queues it on expiry
- **`loom::yield_now()`** — explicit cooperative yield
- **`smarm::sleep(duration)`** — parks the actor; the timer wheel re-queues it on expiry
- **`smarm::yield_now()`** — explicit cooperative yield
- **Allocator preemption** — as above
- **Spawn** — does not yield by default; the new actor is queued and the spawner continues
`std::thread::sleep` inside an actor blocks the entire OS thread and should never be used. Loom
`std::thread::sleep` inside an actor blocks the entire OS thread and should never be used. SMARM
may emit a warning if it can detect this.
### IO thread
@@ -112,7 +112,7 @@ may emit a warning if it can detect this.
A single dedicated IO thread runs an `epoll`/`kqueue` loop. Actors blocking on IO register their
file descriptor and PID; the IO thread moves them back into the global queue when the fd is ready.
A `HashMap<RawFd, Pid>` maps fds to parked actors. Cancellation (actor dies while waiting on IO)
deregisters the fd. This is intentionally simple and not pluggable; Loom is not a general async
deregisters the fd. This is intentionally simple and not pluggable; SMARM is not a general async
executor.
### Communication
@@ -155,7 +155,7 @@ sensible global default.
### Mutex timeout
Every `loom::mutex` lock attempt is mediated by the scheduler. If the lock is not acquired within
Every `smarm::mutex` lock attempt is mediated by the scheduler. If the lock is not acquired within
a configurable timeout, the actor receives a `LockTimeout` error rather than parking forever. This
is a hard runtime guarantee, not a convention. Default timeout is global and configurable;
individual locks and individual call sites can override it.
@@ -165,9 +165,9 @@ individual locks and individual call sites can override it.
Actors can spawn children and wait on a group of handles:
```rust
let h1 = loom::spawn(|| compute_a());
let h2 = loom::spawn(|| compute_b());
let (a, b) = loom::join!(h1, h2);
let h1 = smarm::spawn(|| compute_a());
let h2 = smarm::spawn(|| compute_b());
let (a, b) = smarm::join!(h1, h2);
```
`join!` parks the calling actor until all handles complete. The last child to finish re-queues the
@@ -176,7 +176,7 @@ parent. This is a countdown in the parent's descriptor; no polling, no waker reg
### Timer wheel
`loom::sleep` and supervision timeouts are driven by a timer wheel in the scheduler. Sleeping
`smarm::sleep` and supervision timeouts are driven by a timer wheel in the scheduler. Sleeping
actors are parked and re-queued by the timer thread on expiry. The timer wheel is internal
infrastructure; its design is an implementation detail.
@@ -189,22 +189,29 @@ infrastructure; its design is an implementation detail.
- **Queue contention** — if `Mutex<HashMap>` proves to be a bottleneck under profiling, evaluate
`DashMap` or a lock-free work-stealing deque (e.g. `crossbeam-deque`). Not before.
- **AVX-512 context save** — extend `ContextSaveArea` when there is a concrete use case.
- **`loom::sleep` vs raw sleep semantics** — further control knobs deferred until the basic sleep
- **`smarm::sleep` vs raw sleep semantics** — further control knobs deferred until the basic sleep
is working and real use cases are understood.
- **Supervision tree API** — the contract is defined; the recursive hierarchy, restart strategies,
and introspection API are implementation work.
- **no_std support** — the assembly shim is no_std friendly but the IO thread and allocator require
OS primitives. Target is no_std + `alloc` on hosted platforms; bare metal is out of scope.
- **Distribution**Loom is a single-process runtime. No distribution protocol, no BEAM-style
- **Distribution**SMARM is a single-process runtime. No distribution protocol, no BEAM-style
clustering.
---
## What Loom is Not
## What SMARM is Not
- Not a drop-in replacement for Tokio. Loom does not implement `Future` or the async executor interface.
- Not a general allocator. Loom manages actor stacks; heap allocation for actor data goes through
- Not a drop-in replacement for Tokio. SMARM does not implement `Future` or the async executor interface.
- Not a general allocator. SMARM manages actor stacks; heap allocation for actor data goes through
the system allocator.
- Not Erlang. No hot code reloading, no distribution protocol, no BEAM bytecode. Loom is a
- Not Erlang. No hot code reloading, no distribution protocol, no BEAM bytecode. SMARM is a
concurrency runtime, not a platform.
- Not a real-time scheduler. Timeslice accuracy is best-effort.
---
## On names
<sub>The name is a recursive acronym. The M is for Marks, as in the BEAM — Bogdan/Björn's Erlang Abstract Machine, the virtual machine that runs Erlang and Elixir. smarm is not the BEAM. It just admires it from a safe distance.</sub>

1297
docs/smarm - Deep Dive.html Normal file

File diff suppressed because it is too large Load Diff