Context
PR #1330 removed the previous actor-pool machinery because it was inert and hazardous rather than an optimization:
actor_pool_acquire had zero call sites, so no actor was ever pooled; every core still paid for an initialized 64-slot ActorPool.
- The release path cast
ActorBase* to PooledActor*. The two structs are not layout-compatible (the pool_index read lands inside the step function pointer), and when the overlaid bytes landed in 0..63 the miss-branch routed NUMA-allocated memory to plain free() (mmap-backed under libnuma, VirtualAlloc-backed on Windows NUMA).
- The pool held fixed-size slots with their own embedded
Mailbox, while real actors are variable-size derived structs (sizeof(PingActor) etc.), so the design could not have been wired without a rewrite.
- Surface that lied:
Actor Pooling [ON] in the verbose config print, a documented AETHER_ACTOR_POOL_SIZE env var that fed nothing, a never-incremented actors_pooled counter.
The idea itself is legitimate: high-churn workloads that spawn and retire actors in tight loops (skynet-style trees, per-request actors) pay malloc/free per actor. This issue is the place to decide whether that cost is real and, if so, design a correct pool.
Demand gate (do this first)
Per the project rule that perf work needs profiling evidence before implementation:
- Benchmark spawn/destroy-heavy workloads (skynet in
benchmarks/cross-language/, per-request actor dispatch in std.http) and measure the share of time in scheduler_spawn_actor / scheduler_release_actor allocation.
- If allocation is not a measurable share, close this issue with the numbers.
Design constraints for a real implementation
- Actors are variable-size: a pool must be size-class bucketed or per-actor-type, not fixed-slot.
- NUMA affinity: today spawn allocates on the caller core's node (
aether_numa_alloc); reuse must not silently migrate memory across nodes.
- Work stealing migrates actors between cores mid-life, so release can happen on a different core than spawn; a per-core pool needs a same-node return path (or an ownership rule).
- Reuse must re-initialize the mailbox, atomics,
alloc_size, and panic/dead flags with the same guarantees a fresh calloc/aether_numa_alloc gives.
AETHER_PROFILE plumbing: the removed profile constants (micro/small/medium/large actor-pool sizes) can be revived to size the buckets once something consumes them.
- The
actors_pooled stats counter returns only when it can actually increment.
Acceptance
- Measured improvement on the churn benchmarks, no regression on the message-passing suite.
- Valgrind + ASan clean under spawn/destroy stress.
- Documented in
docs/runtime-optimizations.md with the activation tier stated honestly.
Context
PR #1330 removed the previous actor-pool machinery because it was inert and hazardous rather than an optimization:
actor_pool_acquirehad zero call sites, so no actor was ever pooled; every core still paid for an initialized 64-slotActorPool.ActorBase*toPooledActor*. The two structs are not layout-compatible (thepool_indexread lands inside thestepfunction pointer), and when the overlaid bytes landed in 0..63 the miss-branch routed NUMA-allocated memory to plainfree()(mmap-backed under libnuma, VirtualAlloc-backed on Windows NUMA).Mailbox, while real actors are variable-size derived structs (sizeof(PingActor)etc.), so the design could not have been wired without a rewrite.Actor Pooling [ON]in the verbose config print, a documentedAETHER_ACTOR_POOL_SIZEenv var that fed nothing, a never-incrementedactors_pooledcounter.The idea itself is legitimate: high-churn workloads that spawn and retire actors in tight loops (skynet-style trees, per-request actors) pay malloc/free per actor. This issue is the place to decide whether that cost is real and, if so, design a correct pool.
Demand gate (do this first)
Per the project rule that perf work needs profiling evidence before implementation:
benchmarks/cross-language/, per-request actor dispatch instd.http) and measure the share of time inscheduler_spawn_actor/scheduler_release_actorallocation.Design constraints for a real implementation
aether_numa_alloc); reuse must not silently migrate memory across nodes.alloc_size, and panic/dead flags with the same guarantees a freshcalloc/aether_numa_allocgives.AETHER_PROFILEplumbing: the removed profile constants (micro/small/medium/largeactor-pool sizes) can be revived to size the buckets once something consumes them.actors_pooledstats counter returns only when it can actually increment.Acceptance
docs/runtime-optimizations.mdwith the activation tier stated honestly.