Skip to content

[http-client-csharp] Reduce Roslyn post-processing time (Simplifier.ReduceAsync dominates) #10424

@JoshLove-msft

Description

@JoshLove-msft

Summary

The C# emitter (@typespec/http-client-csharp) spends roughly 70-80% of its CSharpGen.ExecuteAsync wall-clock time inside the Roslyn post-processing block (GeneratedCodeWorkspace.PostProcessAsync + ProcessDocument). The biggest single contributor is Simplifier.ReduceAsync. This issue tracks several concrete optimizations.

Measurements

Captured via the existing LoggingHelpers.LogElapsedTime markers + a temporary per-call Stopwatch around Simplifier.ReduceAsync in GeneratedCodeWorkspace.ProcessDocument:

Project # generated files Total ExecuteAsync Roslyn block Σ Simplifier.ReduceAsync CPU Avg per file
Local/Sample-TypeSpec 138 9.10 s 7.53 s 148.4 s ~1.07 s/file
Spector payload/json-merge-patch 30 5.36 s 3.79 s 8.92 s 297 ms
Spector type/model/inheritance/nested-discriminator 38 5.12 s 3.95 s 15.66 s 412 ms
Spector type/enum/extensible 26 3.87 s 2.87 s 3.96 s 152 ms

Σ Reduce CPU vastly exceeds wall-clock because ProcessDocument runs in Task.WhenAll, but the Roslyn semantic-model lock serializes most of it, so reducing this CPU lowers wall-clock nearly proportionally.

Root causes

  1. Whole-tree Simplifier.Annotation. GeneratedCodeWorkspace.UpdateProject (line ~93) and PostProcessor.MarkInternal (line ~343) call WithAdditionalAnnotations(Simplifier.Annotation) on the entire syntax root. This forces Simplifier.ReduceAsync to walk every node with a full semantic model and probe each reducer (CSharpAliasReducer, CSharpNameReducer, CSharpCastReducer, CSharpExtensionMethodReducer, CSharpParenthesesReducer).

  2. Internalization re-annotates unnecessarily. MarkInternal only flips a public keyword to internal; nothing inside the type becomes reducible. The re-applied annotation triggers redundant work on the second ReduceAsync pass.

  3. Duplicated InternalizeAsync + RemoveAsync. When UnreferencedTypesHandling = RemoveOrInternalize (the common case), both run in sequence. Each re-binds the compilation, re-walks every syntax tree, and re-runs SymbolFinder.FindReferencesAsync over every type. The Remove pass's reference graph is a strict superset of the Internalize pass's.

  4. Sequential SymbolFinder.FindReferencesAsync in ReferenceMapBuilder.BuildPublicReferenceMapAsync / BuildAllReferenceMapAsync (sequential foreach … await over every declared symbol).

  5. Per-node WithSyntaxRoot in InternalizeAsync (foreach (var (model, documentId) in nodesToInternalize) project = MarkInternal(...)) invalidates the compilation cache between every node.

  6. GetSemanticModelAsync in ProcessDocument runs unconditionally, even when there are no rewriters and Simplifier.ReduceAsync is the only consumer.

Proposed optimizations (ranked by leverage)

  1. Targeted Simplifier.Annotation. Annotate only the actual FQN nodes (AliasQualifiedNameSyntax rooted in global:: and their topmost QualifiedNameSyntax ancestor). Drop the root annotation in UpdateProject and MarkInternal. Expected to reduce Simplifier.ReduceAsync cost by an order of magnitude on large libraries.
  2. Drop Simplifier.Annotation in MarkInternal entirely (no reducible content is introduced by an accessibility flip).
  3. Skip Simplifier.ReduceAsync on documents with zero annotated nodes.
  4. Merge InternalizeAsync + RemoveAsync into a single pass that builds one all-references map and derives both sets.
  5. Parallelize SymbolFinder.FindReferencesAsync with Parallel.ForEachAsync and a ConcurrentDictionary-backed ReferenceMap.
  6. Batch MarkInternal per document (one WithSyntaxRoot per document, not per node).
  7. Skip GetSemanticModelAsync in ProcessDocument when no rewriters are registered and the document has no reducible annotations.
  8. Long term: emit short type names + usings in CodeWriter.AppendType so Simplifier becomes a no-op and can be removed entirely (with the semantic-model fetch).

Reproduction

Add a Stopwatch around Simplifier.ReduceAsync in packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs ProcessDocument and emit the cumulative time alongside the existing "Roslyn post processing complete" log line. Run npx tsp compile <spec> --trace @typespec/http-client-csharp --emit . --option @typespec/http-client-csharp.emitter-output-dir=<out> against any of the test projects above.

Metadata

Metadata

Assignees

Labels

emitter:client:csharpIssue for the C# client emitter: @typespec/http-client-csharp

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions