# Week 16 — Update ## TL;DR Refactored publish/receive RPC message memory management to use an arena allocator in zig-libp2p. PR: https://github.com/zen-eth/eth-p2p-z/pull/83/commits/80dec6c68db8f48fc560a15f1580d83813781d46 --- ## Quick summary - Use a short-lived, per-RPC arena to allocate temporary objects used while decoding, validating, and processing messages. - Free the arena once processing completes (success or error) so no individual deallocation is required. - Keep long-lived state (mesh, fanout, peers, interned topic refs) outside the arena. - Measure and tune arena capacity and reuse policy to avoid unbounded growth. --- ## Terminology - Arena: contiguous allocation region where small objects are allocated by bumping a pointer. - ScopedArena: arena whose lifetime is tied to a single RPC handling scope (publish or receive). - Long-lived data: objects that must persist beyond a single RPC (stored on heap or interned pools). - Short-lived data: temporary parsing buffers, intermediate structures, and decoded fields created during RPC handling. --- ## Why an arena fits this workload - RPC handlers allocate many small temporary objects (decoded fields, slices, validation state). - These objects have the same lifetime: the request handling scope. - Arena allocation is O(1) and avoids repeated small heap allocations and frees. - Single release at scope end prevents leaks from forgotten frees and simplifies error paths. --- ## Design overview 1. Per-RPC ScopedArena: - Create a ScopedArena at the start of processing an incoming RPC or when preparing an outgoing RPC. - Use the arena for all ephemeral allocations during decode/validation/processing. - Deallocate by resetting/freeing the ScopedArena at the end of processing. 2. Long-lived allocations: - Mesh/fanout maps, interned topic references, and persistent caches should NOT use the ScopedArena. - Copy or reference durable data out of the arena only when explicitly required (e.g., intern topic string into global pool). 3. Arena pooling: - Maintain a small pool of pre-allocated arenas (or sized buffers) to avoid frequent arena construction costs. - When an arena is returned to the pool, reset it for reuse. - Carefully bound the pool size to avoid unbounded memory. 4. Safety: - Prevent returning pointers into the arena to long-lived structures. - Add debug assertions or runtime checks (in test builds) to catch accidental long-lived references to arena memory. --- ## Typical usage pattern - Receive RPC frame -> allocate ScopedArena (possibly taken from a pool) - Decode message fields into arena-allocated buffers / slices - Validate/transform using arena memory for intermediates - For data that must persist, copy to long-lived allocations or intern them - Send responses / forward; finish processing - Reset or free the ScopedArena (return to pool) --- ## Pseudocode ```pseudo // Handle incoming RPC function handle_rpc(frame): arena = arena_pool.acquire() // or ScopedArena.new() defer arena.reset() // ensure the arena is released at scope exit msg = decode_rpc(frame, arena) // allocations done in arena if !validate(msg): return error // For values that must persist beyond RPC, copy or intern: topic_ref = global_pool.intern(arena.slice_to_bytes(msg.topic)) mesh_map[topic_ref].insert(peer) process_message(msg, arena) // temporary structures used here // when function returns, arena.reset() reclaims all temporary memory ``` Notes: - decode_rpc should allocate decoded strings/slices inside arena. - intern or copy any data that must survive beyond the handler before the arena is reset. --- ## Sequence diagram ```mermaid sequenceDiagram participant Net participant Handler participant ArenaPool as ArenaPool participant Global as GlobalState Net->>Handler: incoming RPC frame Handler->>ArenaPool: acquire() -> ScopedArena Handler->>Handler: decode frame into ScopedArena Handler->>Global: intern(topic) [copy out of arena] Handler->>Handler: process (use arena allocations) Handler->>ArenaPool: reset(release) ScopedArena ``` --- ## Implementation considerations - Arena size and growth: - Start with a moderate initial size (e.g., 8–32 KiB) and grow exponentially for large messages. - Limit max arena size to avoid pathological allocations from a single message. - Pooling strategy: - Keep a small bounded pool of reusable arenas for common concurrency levels. - If no pooled arena is available, create one on demand and consider returning it to the pool afterward. - Thread-safety: - ScopedArena usage is single-threaded per handler. ArenaPool must be thread-safe if handlers run concurrently (use lock-free stack or mutex). - Avoid sharing a ScopedArena between threads. - Defensive programming: - In debug builds, tag arena allocations so attempts to leak pointers (into long-lived state) can be detected. - Provide helper APIs to "promote" arena data into persistent storage (copy or intern) with a clear cost. - Interactions with interned pool: - Always intern topic strings or other identifiers when they must persist. - Intern API should copy from arena-sourced bytes before arena reset. - Error and cancellation paths: - Ensure arena.reset() is invoked on every exit path (use defer/panic handlers). - If processing is canceled mid-way, still release arena resources promptly. --- ## Next Start implementing the ScopedArena and ArenaPool modules, replace ephemeral allocations in publish/receive handlers with arena allocations, and run the unit tests and benchmarks described above.