RFC-0021 — Runtime Performance Optimizations
- Status: Draft (parking lot — not yet prioritized)
- Author: Mark Truluck
- Companion: RFC-0020 (runtime spec realignment — predecessor; must land first)
Summary
A collection of runtime performance optimizations that have come up while discussing the framec runtime shape but that are deliberately not in scope for RFC-0020. RFC-0020’s goal is uniformity across the 17 backends and alignment with the Frame runtime spec; this document collects perf-driven changes that would either deviate from the spec further or impose per-backend complexity that pays off only in some targets.
The items below are draft proposals. Each one is at the “worth thinking about” stage, not “ready to ship.” None are prioritized. The point of this document is to keep them written down so they don’t get forgotten and so they can be evaluated together rather than smuggled in piecemeal.
Items
1. Event pool
Lifted into RFC-0036 (2026-05-22). The motivating signal this parking lot was waiting for arrived — systems use (
no_std, interrupt context, hot paths) where per-event allocation is disqualifying, not just slow. RFC-0036 takes this item as an opt-in, zero-allocation dispatch mode and extends it to the context + compartment storage.
Replace per-dispatch FrameEvent allocation with a fixed-or-growing pool
of pre-allocated event slots. The pool lives on self; FrameContext
holds an index (or generation-counted handle) into the pool rather than
owning the event. Wrappers acquire a slot, fill in message and
parameters, push the context, dispatch, release the slot.
Benefits, per backend:
| Backend | Win |
|---|---|
| C / C++ | Replaces per-call malloc/free (or stack alloc + ctor) with index writes |
| Rust | Removes the Rc::clone-at-boundary that RFC-0020 introduces; handler signature becomes &mut self, event_idx: usize (no aliasing problem at all) |
| Java / C# / Kotlin / Swift | Reduced GC pressure |
| Go | Modest; the allocator is already fast |
| Python / JS / Ruby / PHP / Lua / Dart / GDScript | Roughly neutral — language allocators are tuned for short-lived small objects |
| Erlang | Not applicable — gen_statem owns message flow |
Costs:
- Pool sizing decision (fixed-capacity-with-bail vs. grow-on-demand).
- Slot reclamation discipline — must not reuse a slot while anyone still holds a reference. Single-threaded stack-disciplined dispatch makes this easy, but the invariant has to be documented.
- Non-idiomatic in dynamic languages where short-lived small objects are the normal allocation pattern. Adds code that does nothing useful there.
Open questions:
- Whether to make pooling per-backend opt-in (C / C++ / Rust use a pool; others don’t) or universal (every backend has pool machinery even when its allocator already handles this well).
- Generation counts: if slots are reused, indices need a generation tag to detect use-after-free. Adds bytes per index.
- Synthesized lifecycle events (
<$/$>inside the transition loop) also need to draw from the pool — same machinery applies.
If pooling is adopted universally, Rust’s Rc<FrameEvent> (introduced
by RFC-0020) goes away: events live in the pool, accessed by index, and
the Rust kernel signature becomes the same as everyone else’s
(&mut self, event_idx: usize).
2. State identifier representation — enum / integer instead of string
Today the state name on a compartment is a String/str/etc. Every
dispatch does match self.__compartment.state.as_str() { "Off" => ..., ... }.
Generating an enum (or interning to small integer codes) per system
would let dispatchers be a flat integer match — faster, smaller,
sometimes branch-predictable.
Cross-backend story:
- Rust / C / C++ / Java / C# / Kotlin / Swift / Go: native enum support. Cheapest possible dispatch.
- Python / Ruby / Lua / PHP / JS / TS / Dart / GDScript: integer constants, dictionary-of-handlers, or chained ifs against ints. Still cheaper than string equality.
- Erlang: atoms (which are already interned integers under the hood). No change needed.
Costs:
- Compartment serialization (for
@@[save]/@@[load]): persisted state names need to round-trip across compiles. If we serialize the integer code, a recompile that adds a new state could renumber existing codes and break old blobs. So persisted state must keep a stable string form even if dispatch uses an integer. The mapping table is per-system, emitted at codegen. - Debugging:
match state { 3 => ... }is less readable thanmatch state { "Off" => ... }. Mitigation: emit enum variants with the state name (State::Off = 0,State::On = 1) so the source reads naturally.
This is a bigger change than the pool — it touches the compartment shape, persistence, and every dispatcher. Worth scoping carefully.
3. Further candidates (placeholders, not yet analyzed)
- Handler-method inlining for tiny states. If a state has one
handler, the dispatcher
matchcould collapse to a direct call. - Compartment reuse. When a transition switches state but the source and destination share a parent in HSM, the parent compartment object is rebuilt today. It could be retained.
- Compile-time event-message constants. Per-system enum of message tags instead of string compare in dispatchers (closely related to item 2 — same idea applied to events).
- Persist-blob compactness. The current JSON / dict-based persist shape carries per-field names. A binary or schema-versioned form would shrink saves.
These haven’t been analyzed beyond noting they exist.
Why this is a separate RFC
Each item above is an optimization, evaluated against perf profiles and per-backend cost. RFC-0020 is standardization, evaluated against the canonical Frame runtime spec. The two have different success criteria:
- RFC-0020 succeeds if the 17 backends emit the same conceptual shape and that shape matches the spec.
- RFC-0021’s items succeed only if they demonstrably improve runtime performance enough to justify the per-backend complexity.
Bundling them would conflate the two — making RFC-0020 a “realignment + optimization” RFC and turning it from a clean behavior-preserving refactor into a multi-axis design discussion. The realignment lands cleanly on its own; the optimizations wait for profiling data and a separate decision.
Status
Not prioritized. Items will be lifted out into their own RFCs (or implemented directly under this one) when profiling or another motivating signal makes one of them worth doing.
References
- RFC-0020 — runtime spec realignment; the predecessor that this document is explicitly not doing.
- Frame runtime specification — the canonical contract RFC-0020 aligns to and this document may later evolve.