You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perf(codegen): emit generated source as formatted text - #11658
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
New SourceWriterTests: layout rules, fragment insertion, type spelling against normalized syntax (nested/open/closed generics, tuples, arrays, nullable arrays, pointers, function pointers), string literals with quotes, control characters and Unicode, keyword escaping, and invariant numeric spelling under several non-English cultures.
New GeneratedSourceLayoutTests: a rich source (keyword members and enums, type constraints, nested generics, records, a readonly struct with a private readonly array, hooks, activators, exceptions, cancellation compatibility overloads, response timeouts, observers, provider/alias/id metadata with escapes, a serializer context) in normal and hot-reload modes. Every generated file must compile and match the normalized layout apart from those idiomatic spellings.
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.
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.
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.
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.
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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.UnsafeAccessorfield 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 existingGeneratedTypeConstruction.ProxyEmissionPlan; invokables carry anInvokableEmissionPlan. Metadata names come from plan names and arity instead of reading generated syntax back.CodeText.Type), open-generic rewriting and literal escaping. One rare fragment, a custom-initializer attribute argument, is still normalized on its own.GeneratedProxyDescriptionand unused syntax helpers are removed.Output compatibility
Compared with the current
maingenerator on the captured TestGrainInterfaces compiler inputs (125 sources, 224 references, 415 generated files):typeof(T?),T?[],G<,,>andint[,](notT? ),T? [],G<,, >,int[, ]), named tuple elements such as(int[] items, int? count), and constraint clauses on the declaration line.null !; output is now alwaysnull!).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
RunGeneratorswent 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
SourceWriterTests: layout rules, fragment insertion, type spelling against normalized syntax (nested/open/closed generics, tuples, arrays, nullable arrays, pointers, function pointers), string literals with quotes, control characters and Unicode, keyword escaping, and invariant numeric spelling under several non-English cultures.GeneratedSourceLayoutTests: a rich source (keyword members and enums, type constraints, nested generics, records, a readonly struct with a private readonly array, hooks, activators, exceptions, cancellation compatibility overloads, response timeouts, observers, provider/alias/id metadata with escapes, a serializer context) in normal and hot-reload modes. Every generated file must compile and match the normalized layout apart from those idiomatic spellings.Composition notes
Microsoft Reviewers: Open in CodeFlow