Skip to content

perf(codegen): emit generated source as formatted text - #11658

Merged
ReubenBond merged 3 commits into
dotnet:mainfrom
ReubenBond:rb-text-based-source-emission
Oct 31, 2026
Merged

ReubenBond merged 3 commits into
dotnet:mainfrom
ReubenBond:rb-text-based-source-emission

Conversation

@ReubenBond

@ReubenBond ReubenBond commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

The code generator builds every output file as a Roslyn syntax tree and formats it with NormalizeWhitespace. On RPC-heavy projects that formatting dominates generator time and allocation. This PR writes C# text directly, following the Roslyn incremental-generator cookbook's guidance ("use an indented text writer, not SyntaxNodes") and the approach used by the ASP.NET Core and System.Text.Json generators.

Approach

  • A small internal SourceWriter (StringBuilder, 4-space indentation, CRLF, a blank line after closed blocks) reproduces the layout the normalizer produced, so existing output is preserved rather than reformatted.
  • Serializer, copier, activator and enum renderers consume symbol-free emission plans holding spelled C# text. Member reads/writes and copies are explicit plans (member, accessor invocation, UnsafeAccessor field reference; value, context.DeepCopy, or copier copy with an optional cast). Constructor parameter order, service fields, base contracts and canonical array services come from the existing GeneratedTypeConstruction.
  • Proxy planning runs exactly where the proxy declaration used to be built, so invokable and compatibility-invoker discovery and diagnostics keep their place and order. It produces a ProxyEmissionPlan; invokables carry an InvokableEmissionPlan. Metadata names come from plan names and arity instead of reading generated syntax back.
  • Metadata, assembly attributes and serializer contexts (declaration and implementation) are written directly. Registration order, conflict checks, compound aliases, provider registration and module-initializer behavior are unchanged.
  • Roslyn is still used for input analysis, parsing display strings into type syntax (spelled from tokens by CodeText.Type), open-generic rewriting and literal escaping. One rare fragment, a custom-initializer attribute argument, is still normalized on its own.
  • The whole-file AST assembly, GeneratedProxyDescription and unused syntax helpers are removed.

Output compatibility

Compared with the current main generator on the captured TestGrainInterfaces compiler inputs (125 sources, 224 references, 415 generated files):

  • Hint names and generator diagnostics are identical; the generated compilation has no errors and emits.
  • 398 files are byte-identical and the other 17 are token-identical. Generated source keeps Roslyn's normalized statement layout but uses idiomatic token spelling where the old normalizer padded tokens: typeof(T?), T?[], G<,,> and int[,] (not T? ), T? [], G<,, >, int[, ]), named tuple elements such as (int[] items, int? count), and constraint clauses on the declaration line.
  • Serializer-context output no longer depends on the host Roslyn formatter (some versions wrote null !; output is now always null!).
  • Existing Verify snapshots are unchanged.

Performance

Measured on an earlier composition of this change on top of other in-flight generator PRs, not on this exact head: on the same TestGrainInterfaces inputs, fresh RunGenerators went from about 4.2 s / 850 MB to 0.74-0.80 s / 219 MB, and a retained driver after an unrelated body edit from about 3.7 s / 743 MB to 0.64-0.67 s / 177 MB (same generator target, compiler and inputs; medians of two runs). This is generator-only cost; downstream compilation of the essentially unchanged generated source and whole-build impact were not measured here.

Tests

Composition notes

Microsoft Reviewers: Open in CodeFlow

The generator built each output file as a Roslyn syntax tree and formatted it with
NormalizeWhitespace. On RPC-heavy projects that formatting dominated generator time
and allocation. Emission now writes C# text directly through a small SourceWriter
which reproduces the established layout (four-space indentation, CRLF separators,
a blank line after closed blocks).

- Serializer, copier, activator and enum renderers consume symbol-free emission
  plans holding spelled text. Member access and member copies are explicit plans
  (member, accessor invocation, field reference; value, context, or copier copy).
- Proxy planning runs where the proxy declaration was built, so invokable and
  compatibility-invoker discovery and diagnostics keep their place and order.
  Invokables carry an InvokableEmissionPlan; metadata names come from plan names
  and arity instead of generated syntax.
- Metadata, assembly attributes and serializer contexts are written directly.
- Roslyn still parses display strings into type syntax (spelled from tokens), opens
  generic names and escapes literals.

On the captured TestGrainInterfaces corpus, 410 of 415 files are byte-identical to
the current generator and 5 are token-identical: nullable types before ')' are now
spelled 'T?)' rather than the normalizer's 'T? )'. Hint names, diagnostics and the
compiled output are unchanged.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 14:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

GeneratedSourceOutput.cs is missing the final newline required by .editorconfig, leaving a repository formatting violation unresolved.

1 open issue
What changed in this PR

This PR replaces whole-file Roslyn syntax-tree construction and formatting with direct text emission in the Orleans code generator, aiming to reduce generation time and allocations while preserving output compatibility.

Changes:

  • Adds a formatted source writer, lexical helpers, and symbol-free emission plans.
  • Migrates serializer, copier, activator, RPC, metadata, and serializer-context output to text renderers.
  • Adds layout and compilation coverage and adapts existing construction and registration tests.
File Description
test/​Orleans.CodeGenerator.Tests/​SourceWriterTests.cs Tests layout, type spelling, escaping, and numeric formatting.
test/​Orleans.CodeGenerator.Tests/​SerializerInitializationTests.cs Adapts registration tests to text emission.
test/​Orleans.CodeGenerator.Tests/​SerializerConstructionPlanningTests.cs Checks construction contracts against parsed text output.
test/​Orleans.CodeGenerator.Tests/​GeneratedSourceLayoutTests.cs Adds compilation and layout checks, including hot reload.
src/​Orleans.CodeGenerator/​SyntaxGeneration/​SyntaxFactoryUtility.cs Produces generic constraint text.
src/​Orleans.CodeGenerator/​SyntaxGeneration/​SymbolSyntaxExtensions.cs Removes an unused syntax helper.
src/​Orleans.CodeGenerator/​SyntaxGeneration/​StringExtensions.cs Removes the old literal-expression helper.
src/​Orleans.CodeGenerator/​SyntaxGeneration/​FSharpUtils.cs Converts F# member access to emission plans.
src/​Orleans.CodeGenerator/​SyntaxGeneration/​CodeText.cs Adds lexical spelling and literal helpers.
src/​Orleans.CodeGenerator/​SourceWriter.cs Adds direct, consistently indented source writing.
src/​Orleans.CodeGenerator/​SerializerGenerator.cs Plans and writes serializer declarations.
src/​Orleans.CodeGenerator/​SerializerFactoryGenerator.cs Writes context registrations directly.
src/​Orleans.CodeGenerator/​SerializerContextGenerator.cs Writes context declarations and implementations directly.
src/​Orleans.CodeGenerator/​SerializableSourceOutputGenerator.cs Composes serializer, copier, and activator plans.
src/​Orleans.CodeGenerator/​ProxySourceOutputGenerator.cs Assembles RPC output using text renderers.
src/​Orleans.CodeGenerator/​ProxyGenerator.cs Plans and writes RPC proxies.
src/​Orleans.CodeGenerator/​ProxyGenerationContext.cs Retains emission plans instead of generated syntax.
src/​Orleans.CodeGenerator/​Model/​SerializableTypeEmissionPlan.cs Defines shared serialization emission plans.
src/​Orleans.CodeGenerator/​Model/​ProxyEmissionPlan.cs Defines proxy declarations and method plans.
src/​Orleans.CodeGenerator/​Model/​InvokableEmissionPlan.cs Defines invokable emission data.
src/​Orleans.CodeGenerator/​Model/​GeneratedProxyDescription.cs Removes the obsolete proxy description.
src/​Orleans.CodeGenerator/​Model/​GeneratedInvokableDescription.cs Carries an emission plan instead of class syntax.
src/​Orleans.CodeGenerator/​Model/​EnumSerializerPlan.cs Stores spelled enum fragments and hint identity.
src/​Orleans.CodeGenerator/​MetadataSourceOutputGenerator.cs Composes metadata and assembly-attribute text.
src/​Orleans.CodeGenerator/​MetadataGenerator.cs Writes metadata registrations and provider declarations.
src/​Orleans.CodeGenerator/​LibraryTypes.cs Caches shared library-type spellings.
src/​Orleans.CodeGenerator/​InvokableGenerator.cs Plans and writes RPC invokables.
src/​Orleans.CodeGenerator/​GeneratedSourceOutput.cs Replaces compilation-unit formatting with writer composition.
src/​Orleans.CodeGenerator/​GeneratedCodeUtilities.cs Provides generated attributes as text.
src/​Orleans.CodeGenerator/​EnumSerializerGenerator.cs Writes enum codecs directly.
src/​Orleans.CodeGenerator/​CopierGenerator.cs Plans and writes copier declarations.
src/​Orleans.CodeGenerator/​ApplicationPartAttributeGenerator.cs Emits application-part attributes as text.
src/​Orleans.CodeGenerator/​ActivatorGenerator.cs Plans and writes activator declarations.

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 31, 2026 14:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Named tuple elements and nullable-array context registrations still introduce type-spacing differences outside the documented layout exceptions.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Low severity Canonicalize nullable-array type spelling in context registrations

src/​Orleans.CodeGenerator/​SerializerFactoryGenerator.cs:139

Context registrations still use raw display strings from Name, so a nullable-array root emits options.AddSerializer<int?[]> instead of the established options.AddSerializer<int? []> spelling. This remains valid C#, but introduces another layout difference outside the documented exceptions across context registrations. Canonicalize the shared Name helper with CodeText.Type and add a nullable-array context to the layout test.

Low severity Preserve spacing before named tuple elements

src/​Orleans.CodeGenerator/​SyntaxGeneration/​CodeText.cs:50

Named tuple elements lose spacing after non-word type tokens: (int[] items, int? count) becomes (int[]items, int?count). This remains valid C#, but adds layout differences beyond the PR's documented exceptions. Handle tuple-element identifiers explicitly and add type-spelling cases with array, nullable, and generic element types.

1 resolved since last review

Review effort: Balanced

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Code coverage

Metric Pull request Current main Variance
Lines 84.07% (121,081 / 144,025) 84.06% (121,497 / 144,532) +0.0071 pp
Branches 74.30% (36,611 / 49,277) 74.10% (36,522 / 49,290) +0.2002 pp

Report-only conclusion: improved.

The current-main baseline is commit 040aa5dde1 and uses the same reviewed coverage matrix.

Coverage combines every CI test matrix job, including providers, CodeGen, .NET 8/10, Linux, Windows, and macOS, using canonical physical source and branch identities.

The comparison remains report-only while normal line and branch variance is calibrated.

Coverage details

Direct emission reproduced several whitespace artifacts of the former
NormalizeWhitespace pass. Generated source now uses idiomatic spelling:

- named tuple elements keep their separating space after array, nullable and
  generic element types: (int[] items, int? count) rather than (int[]items, int?count);
- omitted type arguments and array ranks are not padded: G<,,> and int[,]
  rather than G<,, > and int[, ];
- a nullable array is T?[] rather than T? [];
- every constraint clause stays on its declaration line rather than breaking
  after new().

Serializer-context registrations already used the idiomatic display spelling
(int?[]). On the captured TestGrainInterfaces corpus, 398 of 415 files are
byte-identical to main and 17 are token-identical, differing only by these
spellings; hint names, diagnostics and compiled output are unchanged.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 15:20
@ReubenBond

Copy link
Copy Markdown
Member Author

Preserve spacing before named tuple elements

Fixed in 22fbb72: tuple element names now keep their space after array, nullable and generic element types ((int[] items, int? count)), with type-spelling and layout test cases.

Canonicalize nullable-array type spelling in context registrations

Resolved the other way: generated source now uses idiomatic spelling rather than the old normalizer's padding, so int?[] in context registrations is the intended form. The other emitters now match it (T?[], G<,,>, int[,], and constraint clauses on the declaration line). The layout test covers a nullable-array context root.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

There are no active review issues blocking the direct C# text-emission changes.

No open issues 🎉

Review effort: Balanced

@ReubenBond
ReubenBond merged commit 57e5420 into dotnet:main Oct 31, 2026
95 checks passed
@ReubenBond
ReubenBond deleted the rb-text-based-source-emission branch October 31, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants