Skip to content

Avoid unnecessary allocations in ParseInfo / ParseCategorizedInfo in RedisDatabase #669

Open

Description

@LeaFrock

Observations

  • ParseCategorizedInfo allocates a List<InfoDetail>, only to immediately materialize it into an InfoDetail[].
  • ParseInfo then further projects that array into a Dictionary<string, string>.
  • EnumerateLines is currently an extension method, but is only used by this parsing path.

Proposal

  1. Remove the EnumerateLines extension method and move its logic into InfoDetail.
  2. Add an internal static factory method such as:
    internal static List<InfoDetail> Parse(ReadOnlySpan<char> info)
  3. Allow callers to project directly from the List<InfoDetail>:
    • Into an array (InfoDetail[]) where needed
    • Or into a Dictionary<string, string> for ParseInfo

This would:

  • Eliminate one intermediate array allocation
  • Keep parsing logic co-located with InfoDetail
  • Reduce public surface area (even if just internal)
  • Make allocation patterns clearer and easier to reason about

If desired, we could also consider a Dictionary-backed fast-path once the list is built, but even the structural cleanup alone seems worthwhile.

I want to handle this after #664 gets merged when the SpanExtensions.cs can be deleted then.

More...

  • Would it be better if InfoDetail becomes a read-only struct?
  • Add Task<InfoDetail[]> GetInfoSectionAsync(string section) as Redis supports INFO [section], for example, redis.call('INFO', 'memory').

Activity

  1. imperugo commented on Aug 7, 2026

    @imperugo
    Owner

    Good issue — the core of it is straightforward and I'd take it. One part needs splitting off, though.

    The struct question is a breaking change

    Would it be better if InfoDetail becomes a read-only struct?

    InfoDetail is public, and it's exposed through IRedisDatabase:

    // Abstractions/IRedisDatabase.cs:414
    public Task<InfoDetail[]> GetInfoCategorizedAsync();

    Turning a public class into a struct swaps reference semantics for value semantics on a type consumers already hold: null checks and InfoDetail x = null stop compiling, reference equality changes meaning, and it's a binary break on top of the source break. So that one belongs in v14, alongside #663 and the ServerIteratorFactory removal — not here.

    Worth weighing on its own merits too, separately from the versioning. InfoDetail carries three strings, so as a struct it's 24 bytes copied on every pass rather than 8 for the reference, and it's handed out as an array from an admin-path call that just parsed a multi-kilobyte INFO payload. The parsing dominates either way. I'm not against it, but I'd want a reason beyond symmetry before spending a breaking change on it.

    The rest is fine for v13.5

    Moving the parse into InfoDetail, dropping the intermediate array, and shrinking the surface are all internal — nothing observable to consumers. Same for GetInfoSectionAsync(string section): purely additive, and genuinely useful given INFO memory alone is a fraction of the full payload we currently parse and discard.

    Your sequencing is right, and I checked: SpanExtensions.cs currently holds both EnumerateLines and ToFastArray(ReadOnlySpan<T>). #664 moves the latter to GenericsExtensions, which leaves EnumerateLines as the only occupant — so once this issue relocates it into InfoDetail, the file goes away entirely. Clean chain.

    I've put this on v13.5 with the struct question carved out; open a separate issue for that against v14 whenever you want to make the case.

  2. self-assigned this
    on Aug 8, 2026
  3. imperugo commented on Aug 14, 2026

    @imperugo
    Owner

    Recording @LeaFrock's case for InfoDetail as a readonly struct, from the discussion on #670, so it isn't lost when the v14 conversation happens.

    His two arguments:

    1. Memory layout. For an array of N entries, a struct holding three string references is 24N bytes, against roughly 48N for a class once you count the reference plus object header. That compaction is real, and better than the "symmetry" reasoning I dismissed earlier in this issue — I underweighted it.
    2. Semantics. The data originates from the server and is never mutated, so a read-only value type states the intent that a class with get-only properties only implies.

    The counterweights, for whoever picks this up: it stays a source-and-binary breaking change on a type exposed through IRedisDatabase.GetInfoCategorizedAsync() — null checks against InfoDetail stop compiling and reference equality changes meaning. And 24 bytes get copied on every element access, so code doing response.Single(x => x.Key == ...) over a large INFO payload pays for it.

    Not a decision for v13.5 either way. Worth its own issue against v14.0 — Breaking Changes when someone wants to make the call.

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

Metadata

Metadata

Assignees

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions