-
Notifications
You must be signed in to change notification settings - Fork 0
EN 05_synthesis_and_performance
🇺🇸 English | 🇯🇵 日本語 | Introduction
The generator consumes extracted Data Transfer Objects (DTOs), such as DependencyPropertyData, to emit framework-specific C# source files targeting WPF, MAUI, Avalonia, Uno, and WinUI. This generated code extends user-declared partial classes.
Input constraints The generator processes user code that meets the following criteria:
- Classes declared with the
partialmodifier. - Classes decorated with
[DependencyProperty]or related attributes. - Optional
partial void On...Changed()hook declarations.
Output artifacts The generated code includes the following structural elements:
- Static dependency property fields, typically suffixed with
Property. - CLR property wrappers implementing
getandsetaccessors. - Property change callback wiring bound to
propertyChangedCallback. - Comprehensive XML documentation comments.
The engine handles code emission using SourceWriter from the Kassyi.Generators.Extensions namespace. It standardizes structural patterns to eliminate boilerplate, ensuring zero heap allocation and safely managing indentation scopes.
The generator condenses repetitive boilerplate into a single line across all source templates. This envelope includes the #nullable enable directive, the namespace declaration, nested parent classes, and the target partial class definition.
// The ClassScope helper generates the complete outer envelope in one operation.
using var _ = writer.ClassScope(@class);
// Core member generation logic follows.Tip
This pattern guarantees zero memory allocation. The ClassScope method returns a ref struct SourceWriterClassScope. Upon disposal, this struct emits closing braces for all opened nested classes and namespaces without allocating on the heap.
Note
If a target class is nested within outer parent classes (defined in ClassData.ParentClasses), ClassScope opens enclosing partial classes from the outermost to the innermost scope. It automatically closes them in reverse order upon disposal.
The generator passes method or static constructor signatures directly into the Scope method to manage block indentation.
using (writer.Scope($"static {@class.Name}()"))
{
// Static constructor registration statements
}The PrepareData extraction phase automatically expands target-typed new expressions. If a DefaultValueExpression starts with new(...) or new (...) (using C# 9.0+ syntax), the pipeline transforms it into a fully-qualified global type name.
Example transformation:
-
Input:
[DependencyProperty<MyProfile>("Profile", DefaultValueExpression = "new(1.5, 48.0)")] -
Output:
new global::MyNamespace.MyProfile(1.5, 48.0)
This mechanism improves code clarity by eliminating verbose manual namespaces within string literals and increases refactoring resilience when instantiating types from external namespaces.
When user code defines a partial property using C# 13 syntax (for example, public partial int Value { get; set; }), the generator detects Modifiers.IsPartialProperty and emits the implementation block. This transparently supports both standard and modern partial property declarations.
The generator resolves callback signatures using dedicated rule classes in the Rules/Signatures/ directory. The engine strictly enforces parameter limits and type requirements.
Supported signatures:
-
0 parameters: Handled by
NoParametersRule. -
1 parameter: Handled by
SingleParameterRule. Accepts the new value orEventArgs. -
2 parameters: Handled by
DoubleParameterRule. Accepts pairs such as old and new value, sender and new value, or sender andEventArgs. -
3 parameters: Handled by
TripleParameterRule. Accepts sender, old value, and new value.
Warning
The rule engine ignores signatures with 4 or more parameters because it lacks the internal arguments to satisfy them.
// Example of a valid 2-parameter signature:
partial void OnTextChanged(string oldValue, string newValue);
// Example of an unsupported 4-parameter signature:
void OnTextChanged(MyControl sender, string oldValue, string newValue, object extra);The generator enforces strict compilation errors for invalid callback signatures to prevent silent runtime failures.
Explicit specification
If you explicitly define a callback via the OnChanged parameter, an invalid signature or missing method triggers the DPG0001 compilation error, instantly stopping the build.
Convention-based discovery
If you rely on the auto-discovery of partial void On...Changed() methods, an unmatched signature triggers the DPG0007 compilation error.
Important
Elimination of silent callback failure (HavenDV#165)
In the upstream generator, defining a callback with an unsupported signature (like the WPF-standard (DependencyObject, DependencyPropertyChangedEventArgs)) emitted propertyChangedCallback: null without warnings, causing silent runtime failures. This specification prevents these failures by surfacing them immediately as compile-time errors (DPG0001 or DPG0007).
A common cause of the DPG0007 diagnostic is defining a callback using the standard WPF signature with a generic DependencyObject parameter. To enforce type safety, the rule engine explicitly rejects generic DependencyObject arguments.
Note
For specific causes and code examples of how to resolve each diagnostic error (DPG0001 through DPG0008), see 08. Diagnostics reference.
To maintain IDE responsiveness during typing, the architecture enforces strict performance guidelines. Adhere to these principles when extending the generator.
Note
Historical benchmarks and optimization reports
Detailed phase-by-phase benchmark measurements and performance improvement reports conducted on this architecture are documented in tests/Kassyi.Generators.DependencyProperty.Benchmarks (specifically under the Reports/ directory spanning Phase0 through Phase5).
Tip
AST node traversal over string parsing
For expression analysis, use direct ExpressionSyntax Abstract Syntax Tree (AST) traversal. This completely avoids re-tokenization and intermediate syntax tree allocations. It operates significantly faster and uses less memory than re-parsing strings with SyntaxFactory.ParseExpression(). Do not use string re-parsing in generator hot paths.
Tip
SourceWriter over SyntaxFactory for code generation
In code generation hot paths, emit code directly using the custom interpolated string handler SourceWriter. This outperforms heavy syntax tree construction and formatting via SyntaxFactory.NormalizeWhitespace().ToFullString().
Note
You may still use SyntaxFactory in non-hot paths or unit testing environments.
-
Targeted declaration filtering: Use
ForAttributeWithMetadataNameto filter declarations by attribute. This drastically limits generator invocations. Do not use obsolete syntax receivers. -
Early primitive projection: Immediately transform
SyntaxNodeorISymbolinstances into primitives orreadonly record structs during extraction. -
Collection equality: Wrap all collections in
EquatableArray<T>to enforce element-by-element equality checks within DTOs. -
LINQ elimination: Replace LINQ operators (for example,
.Select(),.Where(),.Any()) with indexedforloops in hot extraction and formatting methods to prevent iterator allocations. -
Pre-cache attribute arguments: Cache
NamedArgumentsusing dictionaries to guarantee$O(1)$ property lookups.
Caution
Retaining compilation references
Never retain ISymbol or SyntaxNode in DTOs. This causes severe memory leaks and forces 100% cache misses in the incremental pipeline.
Caution
Mutable collection types
Never use raw List<T> or T[] in DTOs. Their default reference comparisons invalidate the incremental cache.
Warning
Intermediate string allocations
Avoid allocating intermediate strings in hot paths (for example, string.Split() or string.Join()). Use SourceWriter, StringBuilder, and stackalloc Span<char> to prevent GC spikes.
Use the following diagnostic methods to investigate performance bottlenecks in the generator pipeline.
1. MSBuild structured log analysis
Generate a binary log during the build to inspect generator execution time. Analyze the resulting msbuild.binlog using the MSBuild Structured Log Viewer.
dotnet build -c Release -bl:msbuild.binlog2. BenchmarkDotNet execution
Feed synthetic source trees into CSharpGeneratorDriver using BenchmarkDotNet. This accurately measures execution duration and memory allocation across the Gen0, Gen1, and Gen2 heaps.
To validate these architectural shifts, we run continuous benchmarks comparing standard Roslyn techniques against our token streaming approach. For the benchmark source code and detailed measurement methodologies, see the tests/Kassyi.Generators.DependencyProperty.Benchmarks project.
Scenario: Converting a target-typed default expression (new(1, 2, 3)) into an explicit instantiation (new global::System.Collections.Generic.List<string>(1, 2, 3)).
| Method | Mean | Ratio | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|
Roslyn AST mutation (SyntaxFactory) |
16,718.6 ns | 1.00x | 0.6409 | 0.2441 | 0.0610 | 9,712 B | 1.00 |
Direct token streaming (SourceWriter) |
365.4 ns | 0.02x (~46x faster) | 0.0143 | - | - | 240 B | 0.02 (-97.5%) |
-
Roslyn AST mutation:
SyntaxFactory.ParseTypeName→SyntaxFactory.ObjectCreationExpression→.NormalizeWhitespace().ToFullString(). Allocates recursive AST node trees and trivia lists on the heap. -
Direct token streaming: Slices existing tokens or trivia (
ArgumentList,Initializer) directly from the parsed AST and streams them straight into the output buffer without allocating intermediate syntax trees.
Environment: WPF generation, AMD Ryzen 9 7900X
| Phase | Time (ms) | Gen0 | Gen1 | Gen2 | Allocated |
|---|---|---|---|---|---|
| Baseline (old pipeline) | 5.34 ms | 187.5 | 62.5 | 7.8 | 2.87 MB |
| v4 Pipeline | 3.72 ms | 125.0 | 31.2 | - | 2.22 MB |
| Improvement | -30.3% | -33.3% | -50.1% | -100% | -22.6% |
Gen0, Gen1, and Gen2 columns represent GC collections per 1,000 operations. Gen2 full GCs are completely eliminated in both micro and macro runs. Benchmarks for MAUI, Avalonia, and WinUI show similar 20% to 30% pipeline throughput gains.
This wiki is automatically synchronized from spec/ in the repository.
- Introduction
- 01. FAQ & Design Rationale
- 02. Foundation & Domain
- 03. Pipeline Architecture
- 04. Framework Strategies
- 05. Synthesis & Performance
- 06. Complexity Model
- 07. Test Specification
- 08. Diagnostics Reference
- 概要
- 01. 設計思想とFAQ
- 02. 基盤とドメイン
- 03. パイプライン構造
- 04. フレームワーク別生成仕様
- 05. コード生成と最適化
- 06. 計算量モデル
- 07. テスト仕様書
- 08. 診断機能リファレンス