WIP: DO NOT REVIEW — ResponseCaching: avoid one-element string[] allocation on vary-by lookup#67759
WIP: DO NOT REVIEW — ResponseCaching: avoid one-element string[] allocation on vary-by lookup#67759artl93 wants to merge 2 commits into
Conversation
Change the internal IResponseCachingKeyProvider.CreateLookupVaryByKeys to return StringValues instead of IEnumerable<string>. The single-key production path previously allocated a new string[1] solely to enumerate one storage lookup key; StringValues wraps the single string with no array allocation and enumerates via its struct enumerator. Lookup order/behavior is unchanged; the interface is internal. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6666920a-03db-40e8-b536-c813f6f2e267
Cover the vary-by lookup-key semantics the StringValues change must preserve: no-vary (prefix-only) rules, vary-by-header, vary-by-query-key, wildcard query keys, combined header+query, and delimiter validation. Each asserts the lookup yields exactly the single CreateStorageVaryByKey value in order, guarding lookup order/equality against regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6666920a-03db-40e8-b536-c813f6f2e267
Preliminary analysis — deterministic allocation A/B (still WIP)Correction to the PR body estimate: the preliminary body says ~32 B/lookup. Rigorous A/B measurement shows the real saving is 64 B/call, not 32. The old
Where the array came from
Semantics preserved
Before / after (deterministic allocation, per call)
The saving is a constant 64 B independent of vary-rule size, because it is the fixed array-header + boxed-enumerator cost, not a per-key cost. Provenance
When this is hit
Limitations / review risk
Still WIP — do not review. |
Precision note + statusMinor correction to my prior comment for accuracy: the second 32 B in the old path is a heap-allocated reference-type enumerator ( Everything else in the analysis stands and is backed by the deterministic A/B table above. This PR remains WIP — DO NOT REVIEW. It is intentionally left as a draft. Contact @artl93. |
I am running an experiment - full analysis to follow. Contact @artl93
Summary (preliminary)
Response caching vary-rule lookups currently allocate one single-element
string[]per lookup. This change removes that array. Preliminary estimate: ~32 B/lookup eliminated (a length-1string[]on 64-bit), for a 0 B allocation on the focused lookup-key path.Where the array comes from
ResponseCachingMiddleware.TryServeFromCacheAsynccalls the internalIResponseCachingKeyProvider.CreateLookupVaryByKeys(context)andforeach-es the result to probe the cache. The implementation was:i.e. it wraps a single computed storage key in a heap
string[1]purely to satisfy theIEnumerable<string>contract, even though the production implementation only ever yields one key. The experiment changes the internal API to returnMicrosoft.Extensions.Primitives.StringValues(a struct that holds a singlestringwith no array allocation and enumerates via a struct enumerator), preserving the exact lookup order/behavior.Vary-by semantics (why this is safe)
The lookup key is built from
CachedVaryByRules:Hgroup). Header names preserve case; values compared ordinally; multiple values joined by a sub-delimiter.Qgroup; query key names normalized upper-invariant, values sorted ordinally.VaryByQueryKeys = ["*"]varies by all query keys (aggregated case-insensitively by key, values ordinal).VaryByKeyPrefix(the rules GUID), no string builder work.Because the production provider computes exactly one storage key and the middleware probes keys in enumeration order, returning that single key directly (vs. a 1-element array) is order- and behavior-preserving. The interface is
internal, so there is no public API change.Applicability (cache hit/miss)
CreateLookupVaryByKeysruns only when the base-key entry resolves to aCachedVaryByRules— i.e. the resource was previously stored with aVaryheader orVaryByQueryKeys. It fires on the vary-resolution step of such requests (both eventual hits and vary-misses), once per qualifying request.Known gaps in this preliminary post
src/Middleware/perf/ResponseCaching.Microbenchmarks/ResponseCachingBenchmark.cscurrently exercises a non-varyServeFromCachepath, so it does not cover this code. A rigorous, hashed allocation A/B across vary scenarios (no-vary control, single/multiple Vary headers, query-key variation, wildcard*, empty/invalid, hit/miss/store, casing, multi-values, small/large key sets) is in progress and will be posted here.Preliminary — full analysis, canonical before/after tables, and tests to follow.