Skip to content

Startup JIT: consolidate remaining per-file ModuleInitializers into merged partial-class .cctor pattern #6226

Description

@thomhurst

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions