feat(wip): add to design pages

Ran through a few passes of the 'happy path' with the debugger, ran flamegraph analysis: we
are very much overdoing the atomic ring buffer channels. They may be fast for concurrency
across threads, but we don't necessarily need that for every occasion.
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-01-27 20:01:35 +07:00
parent c56a05433f
commit 3e504d3969
5 changed files with 59 additions and 27 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
.vscode/

View file

@ -6,6 +6,11 @@ memory allocator and threading provided by the rust standard library.
We are not building a new erlang/BEAM. Minimal feature set means spawning actor processes, not having supervisiors, lots of process
monitoring tools, prempting, etc.
# FIXME
This is slightly obsolete. Was necessary in order to concentrate on getting the basic skeleton up, now it's a distraction and unreliable.
After getting the benches/etc finished, move this into an `ARCHITECTURE.md` file, have it make sense.
## Actor model
An actor has:
@ -48,30 +53,3 @@ The router is the engine for message delivery. It posesses:
- Its own inbox:
The router possesses its own mpsc queue where references to messages are stored. The router will process this queue by dereferencing and writing directly into the recipient's inbox buffer.
### Misc
A means of providing an emergency overflow without adding much more code complexity. The mutex means
this will not be `no_std` however.
```rust
struct HybridChannel<T> {
// Start with lock-free ring buffer
ring: AtomicRingBuffer<T>,
// When full, spill into a Mutex<VecDeque<T>>
overflow: parking_lot::Mutex<VecDeque<T>>,
// Track overflow frequency to resize ring proactively
overflow_count: AtomicUsize,
}
impl<T> HybridChannel<T> {
fn push(&self, value: T) {
if self.ring.push(value).is_err() {
self.overflow.lock().push_back(value);
self.overflow_count.fetch_add(1, Relaxed);
// Optionally: if overflow_count > threshold, grow ring
}
}
}
```

44
TODOs.md Normal file
View file

@ -0,0 +1,44 @@
### Profiling and Benchmarking
- Research as to modern art on benching and profiling
- Implement an MVP here.
- Bench/profile against a suite of tests selected for generality across actor framework usecases
- Identify hot paths and bottlenecks
- e.g. pretty sure the router is a major bottleneck, what else
- follow through the entire message cycle:
Parent process -> Convert to swactor::Message/Envelope -> Router -> Delivery -> Processing -> etc.
Identify every small detail on which you may be able to improve, any unneeded processing or branching
- (optional) Visualization tools:
- make some pretty stuff for tracing messages, actor activity, router activity, etc.
### Usage
- After benching and profiling, cleaning up the most egregious wrongdoings we will:
- actually implement our own projects in the framework, ones I actually find useful personally
- Optimization pipeline:
- Once we have well-established benches and profiles for general cases, build a set of tools that can auto-optimize
for given use cases. Tuning, for example, the channel buffers, router behavior, message consumption behavior, etc.
### Chores
- go over all the FIXMEs littered about. Add comments.
- add misc features as they come up. Prefer tools for understanding execution flows, visualizing flows, and adding
robustness, over ergonomics. Better to be slightly clunky but fast and optimized, than vice versa.
### Far future
- make language bindings. e.g., an npm package, python bindings, etc.
### Optimization
- Localize actors and inboxes:
- Because the entire runtime is message driven, the happy path must be fast. Even lock free, when we have to go through
several calls of an atomic ring buffer in order to process a single message, its unnecessary.
Parent process -> router -> actor -> router -> inbox -> parent process; every transfer going through an atomic buffer.
- to do this, design heavily around a localized worker thread. Actors on a working thread should have their inbox localized, they
should be 'sticky' to that thread (FILO queue?), and we should route messages based on core locality. Future optimizations can include
a tunable algorithm that puts actors that frequently communicate together on the same thread.

View file

@ -87,6 +87,7 @@ where
fn tick(&mut self, ctx: &Runtime) {
// TODO: WATERLEVEL is hard coded, and so is this message handling scheme. We should
// make it so both are more flexible, with sane defaults.
// FIXME: lots of indirection just to get length on a hot path
let total_messages = self.inbox.len();
let messages_to_process = if total_messages < WATERLEVEL {
total_messages

View file

@ -19,6 +19,14 @@ pub(crate) trait SenderT: Send + Sync {
impl<M: Message> SenderT for Sender<M> {
fn try_send(&self, envelope: Envelope) {
if let Some(msg) = envelope.downcast_ref::<M>() {
// FIXME: we are directly cloning the contents of the Arc pointer here
// Do we want to? Should we provide another way?
//
// The standard concept of an actor has message and state
// isolation, so we should leave this as is. However, we should
// make it clear and obvious this pathway is the heavy, contained
// pathway, and include a shared memory pathway for logic that
// may need it.
let _ = Sender::try_send(self, msg.clone());
}
}