Skip to content

Discovery JIT: shrink generated TestEntry builder IL via shared TUnit.Core factory helpers #6227

@thomhurst

Description

@thomhurst

Background

Per-class generated test sources defer heavy metadata construction behind lazy Entries_0 properties (registered via SourceRegistrar.RegisterEntries<T>, resolved during discovery with parallel pre-resolution in TUnit.Engine/Building/Collectors/AotTestDataCollector.cs). This already moves the JIT cost off the module-init path and spreads it across cores.

However, the Entries_0 builder bodies are the bulk of all generated IL: each one inlines large object-initializer blocks (TestEntry<T>, attribute arrays, filter data, etc.) repeated per test method across every test class. Tier-0 JIT time is roughly linear in IL size, so for large suites the discovery-time JIT cost scales with this repeated boilerplate.

Proposal

Move the repeated construction patterns into shared factory helper methods in TUnit.Core (JIT-compiled once, reused by every generated builder). Generated code then calls helpers passing only the per-test data (strings, constants, delegates) as arguments:

  • A call with N args compiles to far less IL than N inline property-set sequences + newobj chains.
  • The per-test-unique parts (invoker delegates via ldftn, string literals) stay in generated code — they are cheap.
  • Helpers live in TUnit.Core so they can be tiered/optimized once instead of per-class.

Candidates to factor out (audit the emitted Entries_0 shape for the highest-volume patterns first):

  • TestEntry<T> construction with common-default fields
  • Attribute array construction
  • Filter-data field population
  • Repeated metadata sub-object initializers

Expected impact

Smaller IL per generated builder → proportionally less tier-0 JIT time during discovery, especially on large suites and constrained CI agents. Also shrinks compile time and assembly size as a side effect.

Constraints

  • AOT-compatible: helpers must be plain static methods, no reflection; keep [DynamicallyAccessedMembers] annotations flowing where Type parameters are involved.
  • No .Collect()/fan-in changes to the incremental pipeline — this is purely an emit-shape change per file.
  • Dual-mode: reflection mode unaffected, but any shared helper semantics must match what reflection mode produces.
  • Snapshot tests will need re-verification across all TFM output dirs.

Measurement

Before/after with dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime:0x10:4 → PerfView JitStats (per-method JIT ms, IL bytes jitted) on a large test suite.

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