Skip to content

Performance: Reduce allocations in text rendering & interpolations #22

@ZhukMax

Description

@ZhukMax

Cut heap allocations on the hot path of text rendering (labels, headings, buttons, hyperlinks) and template interpolations. Adopt a borrow-first model, reusable buffers, and small containers to minimize churn — especially important for WebAssembly and low-power devices.

  • Fewer allocations → lower GC/allocator pressure, better frame times and battery life.
  • Text + interpolation is a dominant path in typical EFx UIs.
  • A leaner path benefits both native and wasm32-unknown-unknown builds.

Scope

  • Text nodes: <Label>, <Heading>, <Button>, <Hyperlink>, plus any text-emitting tag.
  • Interpolations: sequences like "Hello {name}, {count} unread".
  • Attribute normalization: avoid transient String during parse/map where possible.

Non-goals (here):

  • Changing public semantics or tag behavior.
  • Introducing heavy dependencies by default (feature-gate if needed).
  • Visual/behavioral changes.

Proposed approach

  1. Borrow-first data model (&str/Cow<'a, str>)
    Plumb lifetimes so that text/attribute values stay borrowed whenever possible and only allocate when required (e.g., computed strings, concatenations).
// Example: text segment representation
enum TextSeg<'a> {
  Lit(&'a str),       // borrowed literal slice
  Expr(Cow<'a, str>), // computed, but can be borrowed if source is &str
}
  1. Reusable render buffers (per-frame)
    Introduce a lightweight RenderBuf inside render context (TLS or passed mutably) with pre-grown String buffers reused across nodes:
pub struct RenderBuf {
  pub text: String,       // reused for formatting text nodes
  pub scratch: String,    // misc scratch for adapters
}

impl RenderBuf {
  #[inline] pub fn clear_preserve(&mut self) {
    self.text.clear(); self.scratch.clear();
  }
}
  • Replace format!(...) in hot paths with write!(&mut buf.text, ...) to reuse capacity.
  1. Streaming interpolation
    Compile text nodes to a small iterator over segments; at render time, stitch into RenderBuf::text via write!:
buf.text.clear();
for seg in segs {
  match seg {
    TextSeg::Lit(s) => buf.text.push_str(s),
    TextSeg::Expr(s) => buf.text.push_str(&s),
  }
}
ui.label(&buf.text); // pass &str, no extra allocation
  • Prefer passing &str directly to egui APIs where accepted (Into<WidgetText> from &str avoids another copy).
  1. Small containers for small data
    Use SmallVec<[TextSeg; 4]> (or equivalent) for typical short strings to avoid heap for ≤4 segments.
    Reserve exactly when size is known: vec.reserve_exact(n) to prevent over-allocation.

  2. Avoid transient clones
    Systematically replace .to_string() / .to_owned() / String::from in hot paths with borrowed alternatives or Cow<'_, str>; only allocate on boundary APIs that require owning strings.

  3. Attribute adapters: zero-copy fast path

  • Parse numeric/enum attributes directly from &str via FromStr without intermediate String.
  • Map known keywords through match on byte slices; avoid case conversions unless necessary (or precompute case-folded keys at parse time).
  1. Inlining & micro-tweaks
    Annotate tiny helpers with #[inline]; avoid iterator materialization where for loops generate less overhead; remove temporary Vec where a direct walk suffices.

  2. Benchmark & allocation counting

  • Add criterion benches for:

    • label_literal (short + long).
    • label_interpol_1/3/5 (number of {} segments).
    • button_literal, heading_h1.
    • A small template scene (Column+Row) to approximate real usage.
  • Optional bench-alloc feature: swap global allocator or use a counting wrapper to track allocs/op in benches (build-only, not shipped by default).


Tasks

  • Introduce RenderBuf (reused Strings) and thread it through render context.
  • Switch text interpolation path to streaming write (write!) into RenderBuf.
  • Convert text/attribute representations to borrow-first (&str/Cow<'_, str>).
  • Replace hot-path format!/.to_string() with borrowed or buffered alternatives.
  • Adopt SmallVec (or equivalent) for text segment storage; reserve capacities where known.
  • Optimize attribute adapters: direct parse from &str, keyword match without extra copies.
  • Add criterion benches + (optional) allocation counter under bench-alloc.
  • Document the approach in a brief DEV note: “Why buffers & borrow-first”.
  • Ensure examples build on wasm32-unknown-unknown (no runtime launch).

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions