perf: limit converter discovery to declarations - #6779
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthroughThe AOT converter now resolves ChangesAOT parameter scanning
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Refactor Merge Risk: ⚪ Minimal · up to The generator refinement preserves supported declaration scanning while avoiding unsupported executable-body traversal. No actionable merge risk remains. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit reads each line, Comment |
Reproducing the converter benchmarkBuild Create the project with Add these project items: <ItemGroup>
<Reference Include="TUnit.Core"><HintPath>TUnit.Core.dll</HintPath></Reference>
<None Update="before/*.dll;after/*.dll" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>The benchmark loads both builds in separate Complete benchmark sourceusing BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Text;
using System.Runtime.Loader;
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
[MemoryDiagnoser]
public class ConverterBenchmarks
{
[Params("Bare", "BodyHeavy", "InlineData")]
public string Scenario { get; set; } = "Bare";
private CSharpCompilation _compilation = null!;
private GeneratorDriver _before = null!;
private GeneratorDriver _after = null!;
[GlobalSetup]
public void Setup()
{
var trees = new List<SyntaxTree>();
for (var c = 0; c < 100; c++)
{
var source = new StringBuilder("using System; using TUnit.Core; public class Tests" + c + " {");
for (var m = 0; m < 100; m++)
{
source.Append("[Test]");
source.Append(Scenario == "InlineData" ? "[Arguments(42)] public void Test" : "public void Test");
source.Append(m).Append(Scenario == "InlineData" ? "(int value) {" : "() {");
source.Append("int x = 42; if (x * x + 1 != 1765) throw new Exception();");
if (Scenario == "BodyHeavy")
for (var i = 0; i < 20; i++) source.Append("x = Math.Abs(x) + 1;");
source.Append('}');
}
source.Append('}');
trees.Add(CSharpSyntaxTree.ParseText(source.ToString()));
}
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator)
.Select(p => MetadataReference.CreateFromFile(p)).ToList();
references.Add(MetadataReference.CreateFromFile(typeof(TUnit.Core.TestAttribute).Assembly.Location));
_compilation = CSharpCompilation.Create("SyntheticSuite", trees, references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var errors = _compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
if (errors.Length != 0) throw new InvalidOperationException(string.Join("\n", errors.Select(e => e.ToString())));
_before = LoadDriver("before", Path.Combine(AppContext.BaseDirectory, "before", "TUnit.Core.SourceGenerator.dll"));
_after = LoadDriver("after", Path.Combine(AppContext.BaseDirectory, "after", "TUnit.Core.SourceGenerator.dll"));
var beforeResult = _before.RunGenerators(_compilation).GetRunResult();
var afterResult = _after.RunGenerators(_compilation).GetRunResult();
if (beforeResult.Diagnostics.Concat(afterResult.Diagnostics).Any(d => d.Severity == DiagnosticSeverity.Error))
throw new InvalidOperationException("Generator diagnostics");
if (!beforeResult.GeneratedTrees.Select(t => t.ToString()).SequenceEqual(afterResult.GeneratedTrees.Select(t => t.ToString())))
throw new InvalidOperationException("Generated sources differ");
}
private static GeneratorDriver LoadDriver(string name, string path)
{
var assembly = new AssemblyLoadContext(name).LoadFromAssemblyPath(path);
Console.WriteLine($"{name} generator MVID: {assembly.ManifestModule.ModuleVersionId}");
var generator = (IIncrementalGenerator)Activator.CreateInstance(assembly.GetType("TUnit.Core.SourceGenerator.Generators.AotConverterGenerator")!)!;
return CSharpGeneratorDriver.Create(generator);
}
[Benchmark(Baseline = true)]
public GeneratorDriver Before() => _before.RunGenerators(_compilation);
[Benchmark]
public GeneratorDriver After() => _after.RunGenerators(_compilation);
}Run: dotnet run -c Release -- --filter '*ConverterBenchmarks*' --job Dry --artifacts ./dry
dotnet run -c Release --no-build -- --filter '*ConverterBenchmarks*' --iterationCount 10 --warmupCount 5 --artifacts ./measured --exporters jsonEach operation runs the entire converter generator over 10,000 test methods across 100 classes. Syntax creation, assembly loading, and compilation diagnostics are outside measurement. The initial generator driver is reused without retaining its returned cached output, so each operation performs generation again. This measures generator work, not process startup or compilation of generated code. The three inputs are bare synchronous tests, tests with 20 additional statements in each body, and one |
Greptile SummaryThis PR reduces AOT converter-discovery work by pruning traversal below executable member declarations, resolving
Confidence Score: 5/5The PR appears safe to merge, with declaration-based converter discovery preserved across the covered language constructs. No actionable correctness, security, or repository-rule violations remain; the changed traversal still reaches every syntax and symbol source consumed by converter generation.
|
| Filename | Overview |
|---|---|
| src/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs | Restricts converter scanning to declaration-relevant syntax and replaces repeated formatting and LINQ checks with cached symbol resolution and direct iteration. |
| tests/TUnit.Core.SourceGenerator.Tests/AotConverterScanTests.cs | Adds focused coverage for nested, partial, constructor, method-parameter, and body-only converter-discovery scenarios. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Compilation syntax trees] --> B[Traverse declarations]
B --> C[Test methods]
B --> D[Test classes]
C --> E[Parameters and attributes]
D --> F[Class attributes and constructors]
E --> G[Collect referenced types]
F --> G
G --> H[Find conversion operators]
H --> I[Generate AOT converter registrations]
Reviews (1): Last reviewed commit: "perf: limit converter discovery to decla..." | Re-trigger Greptile
ReviewReviewed the diff in The overall direction is good: pruning descent into method/property/field bodies is a solid perf win for this hot generator path, and switching to symbol equality is more correct than string-comparing display names. I traced through two things that looked risky at first and turned out fine:
One issue worth fixing before merge:
The previous implementation ( Suggested fix: keep the single 🤖 Generated with Claude Code |
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.66.27 to 1.67.0. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.67.0 <!-- Release notes generated using configuration in .github/release.yml at v1.67.0 --> ## What's Changed ### Other Changes * docs: clarified and updated attributes comparison for xUnit 3 by @304NotModified in thomhurst/TUnit#6774 * perf: read inline argument metadata without reflection by @thomhurst in thomhurst/TUnit#6778 * perf: limit converter discovery to declarations by @thomhurst in thomhurst/TUnit#6779 * perf: skip teardown analysis when no disposable members need cleanup by @thomhurst in thomhurst/TUnit#6780 * perf: avoid line allocations when writing generated source by @thomhurst in thomhurst/TUnit#6781 * perf: avoid formatting interface names for data-source checks by @thomhurst in thomhurst/TUnit#6782 * perf: skip unannotated property data-source candidates by @thomhurst in thomhurst/TUnit#6784 * fix: fold inner exceptions into IDE test failure output by @thomhurst in thomhurst/TUnit#6777 * perf: reuse argument-free attribute initializer text by @thomhurst in thomhurst/TUnit#6788 * perf: extract test metadata in attribute transforms by @thomhurst in thomhurst/TUnit#6789 * perf: skip receiver registration for ordinary objects by @thomhurst in thomhurst/TUnit#6790 * perf: cache reporting properties on test contexts by @thomhurst in thomhurst/TUnit#6791 * fix: preserve executor registration, limiter precedence, and timeout classification by @Nice3point in thomhurst/TUnit#6768 ### Dependencies * chore(deps): update tunit to 1.66.27 by @thomhurst in thomhurst/TUnit#6742 * chore(deps): update dependency bunit to 2.10.3 by @thomhurst in thomhurst/TUnit#6745 * chore(deps): update dependency imposter to 0.1.11 by @thomhurst in thomhurst/TUnit#6744 * chore(deps): update dependency microsoft.kiota.abstractions to 2.1.2 by @thomhurst in thomhurst/TUnit#6747 * chore(deps): update dependency microsoft.templateengine.authoring.cli to v10.0.401 by @thomhurst in thomhurst/TUnit#6750 * chore(deps): update dependency fsharp.core to 10.1.401 by @thomhurst in thomhurst/TUnit#6748 * chore(deps): update dependency microsoft.templateengine.authoring.templateverifier to 10.0.401 by @thomhurst in thomhurst/TUnit#6751 * chore(deps): update dependency system.commandline to 2.0.12 by @thomhurst in thomhurst/TUnit#6752 * chore(deps): update dependency dotnet-sdk to v10.0.401 by @thomhurst in thomhurst/TUnit#6754 * chore(deps): update microsoft.extensions to 10.0.12 by @thomhurst in thomhurst/TUnit#6755 * chore(deps): update microsoft.aspnetcore to 10.0.12 by @thomhurst in thomhurst/TUnit#6753 * chore(deps): update dependency microsoft.entityframeworkcore to 10.0.12 by @thomhurst in thomhurst/TUnit#6749 * chore(deps): update mcr.microsoft.com/dotnet/sdk docker tag to v11 by @thomhurst in thomhurst/TUnit#6756 * chore(deps): update dependency microsoft.net.test.sdk to 18.10.0 by @thomhurst in thomhurst/TUnit#6761 * chore(deps): update microsoft.extensions to 10.10.0 by @thomhurst in thomhurst/TUnit#6762 * chore(deps): update react to ^19.3.0 by @thomhurst in thomhurst/TUnit#6763 * chore(deps): update dependency awssdk.sqs to 4.0.100.13 by @thomhurst in thomhurst/TUnit#6764 * chore(deps): update dependency polyfill to 11.3.0 by @thomhurst in thomhurst/TUnit#6765 * chore(deps): update dependency polyfill to 11.3.0 by @thomhurst in thomhurst/TUnit#6766 * chore(deps): update dependency stackexchange.redis to 3.2.0 by @thomhurst in thomhurst/TUnit#6769 * chore(deps): update dependency microsoft.net.stringtools to 18.10.1 by @thomhurst in thomhurst/TUnit#6771 * chore(deps): update dependency dotnet-trace to v10.0.745401 by @thomhurst in thomhurst/TUnit#6773 * chore(deps): bump colord from 2.9.3 to 2.10.0 in /docs by @dependabot[bot] in thomhurst/TUnit#6759 * chore(deps): bump joi from 17.13.4 to 17.13.7 in /docs by @dependabot[bot] in thomhurst/TUnit#6758 * chore(deps): bump js-yaml from 4.3.1 to 4.3.2 in /docs by @dependabot[bot] in thomhurst/TUnit#6757 * chore(deps): update dependency yaml to v2.9.1 by @thomhurst in thomhurst/TUnit#6785 * chore(deps): update verify to 32.0.1 by @thomhurst in thomhurst/TUnit#6786 * chore(deps): update dependency nunit.analyzers to 4.15.0 by @thomhurst in thomhurst/TUnit#6792 ## New Contributors * @304NotModified made their first contribution in thomhurst/TUnit#6774 * @Nice3point made their first contribution in thomhurst/TUnit#6768 ... (truncated) Commits viewable in [compare view](thomhurst/TUnit@v1.66.27...v1.67.0). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
AotConverterGeneratorwalks every syntax node in every test body and repeatedly formats attribute type names while deciding which declarations need converters. Restrict traversal to declarations, resolveBaseTestAttributeonce per compilation, and compare symbols directly. Avoid allocating LINQ predicates while checking methods and classes.Nested classes, partial declarations, constructor parameters, parameter attributes on partial-method implementations, and existing conversion generation are preserved. No runtime execution or public API changes.
Performance evidence
Baseline:
a27cfb34bb. BenchmarkDotNet 0.15.8, 10 measured iterations and 5 warm-up iterations, Roslyn 4.14.0, Windows 11, Intel Core i7-12700K (20 logical processors), .NET 10.0.12 x64. SDK selected by the repository'sglobal.json:11.0.100-preview.7.26381.103.Each operation runs the converter generator over 10,000 tests in 100 classes. Setup creates and validates the compilation, loads the actual old and new generator assemblies in separate
AssemblyLoadContextinstances, and checks identical generated sources. The benchmark prints distinct assembly MVIDs and returns each resulting driver. Both builds are measured side by side, after a successful Dry run, without concurrent builds.Error is BenchmarkDotNet's 99.9% confidence-interval half-width. Timings retain substantial variance, but the allocation reductions and large timing differences are clear. This table is a fresh run after stopping a lingering Docker setup process; earlier exploratory timings are not used here. These are converter-only measurements, excluding parsing, project startup, other generators, and compilation of generated code.
Whole-project rebuild experiment
A separate project references TUnit 1.65.68 and replaces only its Core source-generator assembly with the baseline or modified build. It contains 10,000 bare tests in 100 files. Restore is outside timing; each timed
dotnet build -c Release --no-restorefollows an edit to one file. One warm-up per version is discarded, then five rounds alternate the version order. Both outputs execute exactly 10,000 passing tests.These exploratory rebuild timings are inconclusive. They preceded the background-process cleanup above. The mean improves while the median worsens; noise exceeds the isolated saving for bare tests. This PR claims reduced converter work and allocations, not a demonstrated overall rebuild speedup or elimination of the gap in the framework comparison.
Validation
The complete benchmark source and reproduction steps are posted in a PR comment. Temporary benchmark projects and results are kept outside the repository.