Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 148 additions & 25 deletions src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3578,37 +3578,23 @@ private static void GeneratePerClassTestSource(SourceProductionContext context,
// TestEntry<T>[] array — all entries share the same 3 delegates and are built via the
// shared TestEntryFactory so each call site is a single factory call instead of a
// large object initializer (#6227). The factory also builds each entry's MethodMetadata.
writer.AppendLine($"public static readonly global::TUnit.Core.TestEntry<{classGroup.ClassFullyQualified}>[] Entries = new global::TUnit.Core.TestEntry<{classGroup.ClassFullyQualified}>[]");
writer.AppendLine("{");
writer.Indent();
foreach (var method in classGroup.Methods)
var entryType = $"global::TUnit.Core.TestEntry<{classGroup.ClassFullyQualified}>";
if (ShouldChunkEntries(classGroup))
{
WriteChunkedEntries(writer, classGroup, entryType);
}
else
{
writer.AppendLine($"global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<{classGroup.ClassFullyQualified}>(");
writer.AppendLine($"public static readonly {entryType}[] Entries = new {entryType}[]");
writer.AppendLine("{");
writer.Indent();
writer.AppendRaw(method.TestEntryDataFieldsCode);
if (method.TestDataSourcesCode != null)
foreach (var method in classGroup.Methods)
{
writer.AppendLine($"testDataSources: {method.TestDataSourcesCode},");
WriteTestEntryFactoryCall(writer, classGroup, method, ",");
}
if (method.ClassDataSourcesCode != null)
{
writer.AppendLine($"classDataSources: {method.ClassDataSourcesCode},");
}
if (method.DependenciesCode != null)
{
writer.AppendLine($"dependencies: {method.DependenciesCode},");
}
writer.AppendRaw(method.MethodMetadataArgumentsCode);
writer.AppendLine("classMetadata: __classMetadata,");
writer.AppendLine("createInstance: __createInstance,");
writer.AppendLine("invokeBody: __invoke,");
writer.AppendLine($"methodIndex: {method.MethodIndex},");
writer.AppendLine("createAttributes: __attributes,");
writer.AppendLine($"attributeGroupIndex: {method.AttributeGroupIndex}),");
writer.Unindent();
writer.AppendLine("};");
}
writer.Unindent();
writer.AppendLine("};");

writer.Unindent();
writer.AppendLine("}");
Expand All @@ -3634,6 +3620,143 @@ private static void GeneratePerClassTestSource(SourceProductionContext context,
}
}

// Nested-construction entries per __FillEntriesN method. Measured on a 10,000 test
// data-driven suite: 10 minimised total JIT time (5 and 25 were both slower).
private const int NestedEntriesPerFillMethod = 10;

// Total entries per __FillEntriesN method, bounding runs of plain entries in a chunked class.
// Plain entries JIT at roughly linear cost (~6ms per 100-entry plain class), while each extra
// fill method costs roughly 0.15ms (from the 1-entry-per-method measurement), so 100 keeps
// plain runs at the measured plain-class shape for about 2.5% extra method overhead.
private const int EntriesPerFillMethod = 100;

/// <summary>
/// True when an entry builds nested objects (data source attributes, parameter metadata,
/// dependencies, return types) while the outer factory call's arguments are still on the
/// evaluation stack. The JIT spills those pending arguments into fresh temporaries at every
/// nested call, so the temporaries grow with the number of such entries in the method and its
/// JIT time grows faster than linearly (~60ms per 100-entry class for [Arguments] tests).
/// Plain entries pass only constants and cached fields, so they add no spill temporaries.
/// </summary>
private static bool HasNestedConstruction(TestMethodSourceCode method)
{
return method.TestDataSourcesCode != null
|| method.ClassDataSourcesCode != null
|| method.DependenciesCode != null
|| method.MethodMetadataArgumentsCode.Length > 0;
}

/// <summary>
/// True when the class has more nested-construction entries than one fill method holds.
/// Plain entries never trigger chunking, so a large class of plain tests with a few
/// data-driven ones keeps its single array initializer.
/// </summary>
private static bool ShouldChunkEntries(ClassTestGroup classGroup)
{
var nested = 0;
foreach (var method in classGroup.Methods)
{
if (HasNestedConstruction(method) && ++nested > NestedEntriesPerFillMethod)
{
return true;
}
}

return false;
}

/// <summary>
/// Builds the Entries array in fill methods instead of one static constructor array initializer.
/// Each fill method holds at most <see cref="NestedEntriesPerFillMethod"/> nested-construction
/// entries and at most <see cref="EntriesPerFillMethod"/> entries in total. Plain entries are
/// cheap, so runs of them share a fill method with nested entries up to the larger total cap.
/// A new fill method starts when the next entry would exceed either limit.
/// </summary>
private static void WriteChunkedEntries(CodeWriter writer, ClassTestGroup classGroup, string entryType)
{
var methods = classGroup.Methods.AsArray();

// Start index of each fill method, in entry order.
var chunkStarts = new List<int> { 0 };
var nestedInChunk = 0;
var entriesInChunk = 0;
for (var i = 0; i < methods.Length; i++)
{
var nested = HasNestedConstruction(methods[i]);
if (entriesInChunk == EntriesPerFillMethod
|| (nested && nestedInChunk == NestedEntriesPerFillMethod))
{
chunkStarts.Add(i);
nestedInChunk = 0;
entriesInChunk = 0;
}

entriesInChunk++;
if (nested)
{
nestedInChunk++;
}
}

writer.AppendLine($"public static readonly {entryType}[] Entries = __CreateEntries();");
writer.AppendLine($"private static {entryType}[] __CreateEntries()");
writer.AppendLine("{");
writer.Indent();
writer.AppendLine($"var entries = new {entryType}[{methods.Length}];");
for (var chunk = 0; chunk < chunkStarts.Count; chunk++)
{
writer.AppendLine($"__FillEntries{chunk}(entries);");
}
writer.AppendLine("return entries;");
writer.Unindent();
writer.AppendLine("}");

for (var chunk = 0; chunk < chunkStarts.Count; chunk++)
{
writer.AppendLine($"private static void __FillEntries{chunk}({entryType}[] entries)");
writer.AppendLine("{");
writer.Indent();
var end = chunk + 1 < chunkStarts.Count ? chunkStarts[chunk + 1] : methods.Length;
for (var i = chunkStarts[chunk]; i < end; i++)
{
writer.Append($"entries[{i}] = ");
WriteTestEntryFactoryCall(writer, classGroup, methods[i], ";");
}
writer.Unindent();
writer.AppendLine("}");
}
}

/// <summary>
/// Writes one TestEntryFactory.CreateWithClassMetadata call followed by <paramref name="terminator"/>.
/// </summary>
private static void WriteTestEntryFactoryCall(CodeWriter writer, ClassTestGroup classGroup, TestMethodSourceCode method, string terminator)
{
writer.AppendLine($"global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<{classGroup.ClassFullyQualified}>(");
writer.Indent();
writer.AppendRaw(method.TestEntryDataFieldsCode);
if (method.TestDataSourcesCode != null)
{
writer.AppendLine($"testDataSources: {method.TestDataSourcesCode},");
}
if (method.ClassDataSourcesCode != null)
{
writer.AppendLine($"classDataSources: {method.ClassDataSourcesCode},");
}
if (method.DependenciesCode != null)
{
writer.AppendLine($"dependencies: {method.DependenciesCode},");
}
writer.AppendRaw(method.MethodMetadataArgumentsCode);
writer.AppendLine("classMetadata: __classMetadata,");
writer.AppendLine("createInstance: __createInstance,");
writer.AppendLine("invokeBody: __invoke,");
writer.AppendLine($"methodIndex: {method.MethodIndex},");
writer.AppendLine("createAttributes: __attributes,");
writer.AppendLine($"attributeGroupIndex: {method.AttributeGroupIndex}){terminator}");
writer.Unindent();
}

private enum TestReturnPattern
{
Void, // void methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,9 +552,17 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
private static readonly global::System.Func<global::System.Type[], object?[], global::TUnit.TestProject.ArgsAsArrayTests> __createInstance = __CreateInstance;
private static readonly global::System.Func<global::TUnit.TestProject.ArgsAsArrayTests, int, object?[], global::System.Threading.CancellationToken, global::System.Threading.Tasks.ValueTask> __invoke = __Invoke;
private static readonly global::System.Func<int, global::System.Attribute[]> __attributes = __Attributes;
public static readonly global::TUnit.Core.TestEntry<global::TUnit.TestProject.ArgsAsArrayTests>[] Entries = new global::TUnit.Core.TestEntry<global::TUnit.TestProject.ArgsAsArrayTests>[]
public static readonly global::TUnit.Core.TestEntry<global::TUnit.TestProject.ArgsAsArrayTests>[] Entries = __CreateEntries();
private static global::TUnit.Core.TestEntry<global::TUnit.TestProject.ArgsAsArrayTests>[] __CreateEntries()
{
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
var entries = new global::TUnit.Core.TestEntry<global::TUnit.TestProject.ArgsAsArrayTests>[13];
__FillEntries0(entries);
__FillEntries1(entries);
return entries;
}
private static void __FillEntries0(global::TUnit.Core.TestEntry<global::TUnit.TestProject.ArgsAsArrayTests>[] entries)
{
entries[0] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "Params",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.Params",
filePath: "",
Expand All @@ -573,8 +581,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 0,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[1] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "ParamsEnumerable",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.ParamsEnumerable",
filePath: "",
Expand All @@ -593,8 +601,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 1,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[2] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "Following_Non_Params",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.Following_Non_Params",
filePath: "",
Expand All @@ -614,8 +622,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 2,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[3] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "NonParamsStringArray",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsStringArray",
filePath: "",
Expand All @@ -636,8 +644,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 3,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[4] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "NonParamsStringArray_SingleValue",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsStringArray_SingleValue",
filePath: "",
Expand All @@ -657,8 +665,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 4,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[5] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "NonParamsStringArray_MultipleValues",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsStringArray_MultipleValues",
filePath: "",
Expand All @@ -678,8 +686,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 5,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[6] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "NonParamsIntArray",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsIntArray",
filePath: "",
Expand All @@ -699,8 +707,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 6,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[7] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "GenericStringArray",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.GenericStringArray",
filePath: "",
Expand All @@ -720,8 +728,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 7,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[8] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "NonParamsStringArray_BeyondStaticCaseCap",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.NonParamsStringArray_BeyondStaticCaseCap",
filePath: "",
Expand All @@ -741,8 +749,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 8,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[9] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "TrailingArray_ObjectArrayFromMatrix",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.TrailingArray_ObjectArrayFromMatrix",
filePath: "",
Expand All @@ -762,8 +770,11 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 9,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
}
private static void __FillEntries1(global::TUnit.Core.TestEntry<global::TUnit.TestProject.ArgsAsArrayTests>[] entries)
{
entries[10] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "TrailingParamsArray_ObjectArrayFromMatrix",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.TrailingParamsArray_ObjectArrayFromMatrix",
filePath: "",
Expand All @@ -783,8 +794,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 10,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[11] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "TrailingArray_NonZeroLowerBoundArrayFromMatrix",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.TrailingArray_NonZeroLowerBoundArrayFromMatrix",
filePath: "",
Expand All @@ -804,8 +815,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 11,
createAttributes: __attributes,
attributeGroupIndex: 0),
global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
attributeGroupIndex: 0);
entries[12] = global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata<global::TUnit.TestProject.ArgsAsArrayTests>(
methodName: "ParamsObjectArray_ArrayArgumentStaysSingleElement",
fullyQualifiedName: "TUnit.TestProject.ArgsAsArrayTests.ParamsObjectArray_ArrayArgumentStaysSingleElement",
filePath: "",
Expand All @@ -825,8 +836,8 @@ internal static class TUnit_TestProject_ArgsAsArrayTests__TestSource
invokeBody: __invoke,
methodIndex: 12,
createAttributes: __attributes,
attributeGroupIndex: 0),
};
attributeGroupIndex: 0);
}
}
internal static partial class TUnit_TestRegistration
{
Expand Down
Loading
Loading