Skip to content

Runtime: actor pooling (demand-gated), reuse actor allocations under spawn/destroy churn #1332

Description

@nicolas-maman

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions