Background
The test/hook registration path already minimizes startup JIT: per-file generated output contributes static field initializers to partial classes (TUnit.Generated.TUnit_TestRegistration / TUnit_HookRegistration), the compiler merges them into a single .cctor (one JIT-compiled method per assembly), triggered by the single per-assembly [ModuleInitializer] in InfrastructureGenerator. Registrations only store lazy factories, so heavy metadata construction is deferred to discovery (where it JITs in parallel).
Several generators still emit a separate [ModuleInitializer] method per file/class, each costing a type load + a serial JIT at module load:
TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs — 4 emit sites (~lines 589, 762, 800, 886). Worse: the InitializerPropertyRegistry.Register initializer constructs the full InitializerPropertyInfo[] (including delegates) eagerly inline in module init, not lazily.
TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs:656
TUnit.Core.SourceGenerator/CodeGenerators/StaticPropertyInitializationGenerator.cs:211
TUnit.Core.SourceGenerator/CodeGenerators/DynamicTestsGenerator.cs:105 (minor — usually few dynamic sources)
Proposal
Apply the existing consolidation pattern to these generators:
- Emit per-file static field initializers on shared partial shell classes (e.g.
TUnit.Generated.TUnit_PropertyRegistration), declared once by InfrastructureGenerator and triggered via RuntimeHelpers.RunClassConstructor from the existing single module initializer.
- Register lazy factories (
Func<...> with static lambdas) instead of constructing registration payloads eagerly — particularly the InitializerPropertyInfo[] arrays in PropertyInjectionSourceGenerator.
This is free aggregation via the C# compiler's partial-class merge — no .Collect() fan-in is added to the incremental pipeline, so incremental compilation behavior is unchanged.
Expected impact
N per-file module-initializer methods (type load + serial tier-0 JIT each) collapse into one merged .cctor per concern. For large suites with many property-injected/initialized classes this removes a linear serial JIT cost from module load.
Notes
- Keep generated registration bodies loop-free (loops in module-init/.cctor paths can trigger OSR tier-1 compilation of large methods).
- Dual-mode rule applies: source-gen consolidation only; reflection mode unaffected.
- Snapshot tests will need re-verification.
Background
The test/hook registration path already minimizes startup JIT: per-file generated output contributes static field initializers to partial classes (
TUnit.Generated.TUnit_TestRegistration/TUnit_HookRegistration), the compiler merges them into a single.cctor(one JIT-compiled method per assembly), triggered by the single per-assembly[ModuleInitializer]inInfrastructureGenerator. Registrations only store lazy factories, so heavy metadata construction is deferred to discovery (where it JITs in parallel).Several generators still emit a separate
[ModuleInitializer]method per file/class, each costing a type load + a serial JIT at module load:TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs— 4 emit sites (~lines 589, 762, 800, 886). Worse: theInitializerPropertyRegistry.Registerinitializer constructs the fullInitializerPropertyInfo[](including delegates) eagerly inline in module init, not lazily.TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs:656TUnit.Core.SourceGenerator/CodeGenerators/StaticPropertyInitializationGenerator.cs:211TUnit.Core.SourceGenerator/CodeGenerators/DynamicTestsGenerator.cs:105(minor — usually few dynamic sources)Proposal
Apply the existing consolidation pattern to these generators:
TUnit.Generated.TUnit_PropertyRegistration), declared once byInfrastructureGeneratorand triggered viaRuntimeHelpers.RunClassConstructorfrom the existing single module initializer.Func<...>withstaticlambdas) instead of constructing registration payloads eagerly — particularly theInitializerPropertyInfo[]arrays in PropertyInjectionSourceGenerator.This is free aggregation via the C# compiler's partial-class merge — no
.Collect()fan-in is added to the incremental pipeline, so incremental compilation behavior is unchanged.Expected impact
N per-file module-initializer methods (type load + serial tier-0 JIT each) collapse into one merged
.cctorper concern. For large suites with many property-injected/initialized classes this removes a linear serial JIT cost from module load.Notes