Skip to content

perf: explore assembly-attribute discovery as alternative to module initializers #5396

@thomhurst

Description

@thomhurst

Summary

Suggested by @teo-tsirpanis in dotnet/runtime#126541 (comment)

Instead of using [ModuleInitializer] + RunClassConstructor to register test classes, the source generator could emit per-class assembly attributes:

[assembly: TestClass(typeof(MyClass))]

The engine would discover test classes via assembly.GetCustomAttributes<TestClassAttribute>() — pure metadata enumeration with zero .cctor triggers and zero JIT overhead at startup.

Proposed attribute

[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class TestClassAttribute(
    [DynamicallyAccessedMembers(
        DynamicallyAccessedMemberTypes.PublicMethods 
        | DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) : Attribute
{
    [DynamicallyAccessedMembers(
        DynamicallyAccessedMemberTypes.PublicMethods 
        | DynamicallyAccessedMemberTypes.PublicConstructors)]
    public Type TestClassType => type;
}

Benefits over current approach

  • No module initializer at all — eliminates the TUnitInfrastructure.Initialize() + RunClassConstructor chain entirely
  • Metadata-only discovery — assembly attributes are embedded in PE metadata, readable without JIT
  • Incremental-gen friendly — each class emits its own [assembly: TestClass(...)], no Collect needed
  • Natural fit for Phase 2 filter data split (perf: split filter data into separate lightweight class to avoid full .cctor during discovery #5394) — the engine could enumerate type names from assembly attributes before triggering any source-generated code

Considerations

  • Larger architectural change than the lazy registration approach (which already solves the cold-start problem)
  • Would need source-generated proxy/factory classes per test class for the actual test metadata
  • The engine's discovery pipeline would need to change from "iterate Sources.TestEntries" to "scan assembly attributes then resolve per-class"
  • Current lazy registration (perf: defer per-class JIT via lazy test registration + parallel resolution #5395) already makes startup O(1) JIT — this would be an incremental improvement

Context

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