Skip to content

Enum combo rows reflect and allocate on every frame #385

Description

@matt-edmondson

What

Three widget entry points call Enum.GetNames / Enum.GetValues inside the draw path, so every enum combo allocates two fresh arrays per frame, per row, for the life of the application. In an immediate-mode library this is the hot path by definition — a settings panel with a handful of enum rows at 60 fps allocates thousands of arrays a second, all immediately garbage.

Site Calls
ImGui.Widgets/Combo.cs:23,25 Enum.GetValues(typeof(TEnum)), Enum.GetNames(typeof(TEnum))
ImGui.Widgets/EnumCombo.cs:44 Enum.GetValues<T>()
ImGui.Widgets/PropertyGridRows.cs:342-343 Enum.GetNames<TEnum>(), Enum.GetValues<TEnum>()

Combo<TEnum> is the worst of the three, because it uses the non-generic overload:

Array possibleValues = Enum.GetValues(typeof(TEnum));
int currentIndex = Array.IndexOf(possibleValues, selectedValue);
...
selectedValue = (TEnum)possibleValues.GetValue(currentIndex)!;

Array.IndexOf(Array, object) boxes selectedValue and every element it compares against, and GetValue boxes again on selection. So a single Combo<TEnum> costs two array allocations plus a boxing allocation per element scanned, every frame.

Combo<TString> (Combo.cs:50) has the same shape for a different reason — it rebuilds the display array with LINQ and calls ToString on every element each frame:

string[] possibleValuesNames = [.. possibleValues.Select(e => e.ToString(CultureInfo.InvariantCulture))];

Suggested fix

Cache per closed generic type in a static holder, which for enums is free after first use because the values never change:

private static class EnumCache<T> where T : struct, Enum
{
    public static readonly T[] Values = Enum.GetValues<T>();
    public static readonly string[] Names = Enum.GetNames<T>();
}

Then Combo<TEnum> can drop to the generic overload and index without boxing. PropertyGrid.Enum and EnumCombo read the same cache.

The Combo<TString> overload needs a different treatment since its collection is a caller argument — either document that the caller should hoist the name array, or key a small cache on the collection instance.

Notes

  • Behaviour should be unchanged. Worth keeping the existing index >= 0 guard in PropertyGrid.Enum, which is what makes a value outside the declared members (a cast integer, a [Flags] combination) render as no selection rather than throw.
  • No public API change.

Activity

  1. matt-edmondson commented on Sep 9, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    • Priority: Medium — no incorrect behavior, but a real per-frame allocation cost in an immediate-mode library where this code runs every frame a combo is visible.
    • Effort: Small — the fix is a per-closed-type static cache plus swapping Combo<TEnum> to the generic Enum.GetValues<T>()/boxing-free path. No public API change, no blockers.

    Plan

    Add a private generic cache class (e.g. EnumCache<T> where T : struct, Enum) holding Values/Names computed once per closed type, and read it from all three sites: Combo<TEnum> (Combo.cs), EnumCombo (EnumCombo.cs), and PropertyGridRows.cs's enum row. Switch Combo<TEnum> from the non-generic Enum.GetValues(typeof(TEnum))/Array.IndexOf(Array, object) path to the generic, boxing-free equivalent.

    Acceptance criteria

    • No behavior change: existing Combo/EnumCombo/PropertyGrid.Enum tests continue to pass unmodified, including the index >= 0 guard that lets an out-of-range value (cast int, [Flags] combination) render as "no selection."
    • No allocation from Enum.GetValues/Enum.GetNames/boxing on the second and subsequent draws of a given enum type (verify by inspection or a simple allocation-count check in a widget test).
    • Combo<TString> is out of scope for this issue (its collection is caller-supplied, so it needs a different treatment — document as a separate concern rather than folding it in here).

    No blockers or dependencies.


    Generated by Claude Code

  2. matt-edmondson commented on Sep 13, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    Category: Improvement
    Priority: Medium
    Suggested assignment: none — no specific team/area indicated
    Possible duplicate: none found


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions