perf(engine): reduce allocations in reflection-mode discovery/execution by thomhurst · Pull Request #6113 · thomhurst/TUnit (original) (raw)
Cuts per-call/per-method/per-test allocations on the reflection runtime fallback path (no behaviour change):
- IsCovariantCompatible: hoist the covariant-interface list to a static readonly array and check argType separately instead of Concat([argType]).ToArray() per call.
- ReflectionMetadataBuilder / ReflectionHookDiscoveryService: replace GetParameters().Select(...).ToArray() with manual loops into a pre-sized ParameterMetadata[].
- ReflectionHookDiscoveryService hook ordering: pre-compute (minOrder, metadataToken) sort keys into a struct array and Array.Sort instead of an OrderBy lambda that allocated a List and recomputed attributes per comparison. (order, token) is a total key so order semantics are preserved.
- PropertyInjector caching paths: resolve the injected-arguments dictionary once and reuse it for the early-out guard and the add, collapsing the redundant ContainsKey-property + GetOrCreate lookups. Injection guard logic unchanged.
- ConstructorHelper: foreach+break instead of Where().ToArray() to find the [TestConstructor]-marked ctor.
- ReflectionTestDataCollector: unify the ReflectionTypeLoadException fallback onto the shared ArrayPool streaming filter; add StringComparison.Ordinal to ShouldScanAssembly comparisons.
- TestDiscoveryService: project tests onto a pre-sized List instead of allTests.Select(t => t.Context).
Closes #6105
[](/apps/claude)
- Consolidate the two diverging reflection BuildParameterMetadata impls (ReflectionMetadataBuilder + ReflectionHookDiscoveryService) into a shared ParameterMetadataFactory. Behavioural differences (name fallback, IsNullable computation, AOT RequiresUnreferencedCode) are preserved via explicit parameters so observable output is unchanged.
- Collapse BuildParameterMetadata / BuildConstructorParameterMetadata into the single factory entry with a nameFallback parameter (null => "param{index}", "unnamed" for methods).
- PropertyInjector: restore the cheap early-out in the two ResolveAndCache* caching paths. The perf pass eagerly called GetOrCreateInjectedPropertyArguments() upfront, materialising the per-test dictionary even when the property resolved to null. Now read the empty read-only singleton via the public property for the ContainsKey guard and only create the dictionary when actually storing a value.
- TestDiscoveryService.ToContextList: make the comment honest by explaining the actual win — TestDiscoveryContext.AddTests stores an IReadOnlyList directly and only ToArray()s otherwise, so passing a pre-sized List avoids the Select enumerator + ToArray copy.
[](/apps/claude)
thomhurst deleted the perf/issue-6105-reflection-allocs branch
This was referenced
May 29, 2026
This was referenced
Jun 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
[ Show hidden characters]({{ revealButtonHref }})