perf(sourcegen): real InterfaceCache + single-pass attribute classification (original) (raw)

Summary

TUnit.Core.SourceGenerator has a no-op "cache" and repeated LINQ passes over the same attribute arrays per test method. Build-time, but affects IDE responsiveness on every keystroke that touches a test. Found during a hot-path modernization sweep.

Findings

1. InterfaceCache caches nothing — HIGH

TUnit.Core.SourceGenerator/Helpers/InterfaceCache.cs:15-84
Despite the name, ImplementsInterface/GetGenericInterface/IsAsyncEnumerable/IsEnumerable all walk type.AllInterfaces (allocates ImmutableArray) on every call, plus .Any(lambda) closures. Called per-attribute per test method — same shared attribute symbols re-walked repeatedly.
Add a ConcurrentDictionary<ITypeSymbol, ImmutableHashSet<string>> keyed on SymbolEqualityComparer.Default. Note: clear between compilations / key on Compilation to avoid cross-compilation contamination in long IDE sessions.

2. TestMetadataGenerator.CollectConcreteInstantiations — 6-8 LINQ passes over same MethodAttributes — HIGH

TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs:623-790
The same MethodAttributes array is .Where(...).ToArray()-scanned 6-8 times (Arguments, MethodDataSource ×2, data-source, generic ×2). Replace with a single classification foreach partitioning into named buckets (null-init lists).

3. MethodExtensions.GetTestAttribute lambda alloc per method — LOW

TUnit.Core.SourceGenerator/Extensions/MethodExtensions.cs:22FirstOrDefault(lambda) per test method. foreach + early return.

4. CodeWriter.GetIndentation Enumerable.Repeat — LOW

TUnit.Core.SourceGenerator/CodeWriter.cs:51 — cache-miss path only (static cache warms after ~9 calls). new string(' ', n). Cosmetic.

Notes

Acceptance