diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f0ef35..f9bc783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.4] - 2025-08-03 + +### Fixed + +- Corrected the package release notes to include the repository URL. +- Restored AddEmbeddedAttributeDefinition Stage + +### Changed + +- Improved the EquatableImmutableArray instance cache (for most cases) + +### Added + +- Include Generator name and version in the "auto-generated" source code comment. + ## [1.0.3] - 2025-07-28 ### Removed @@ -75,7 +90,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -[Unreleased]: https://github.com/datacute/IncrementalGeneratorExtensions/compare/1.0.3...develop +[Unreleased]: https://github.com/datacute/IncrementalGeneratorExtensions/compare/1.0.4...develop +[1.0.4]: https://github.com/datacute/IncrementalGeneratorExtensions/releases/tag/1.0.4 [1.0.3]: https://github.com/datacute/IncrementalGeneratorExtensions/releases/tag/1.0.3 [1.0.2]: https://github.com/datacute/IncrementalGeneratorExtensions/releases/tag/1.0.2 [1.0.1]: https://github.com/datacute/IncrementalGeneratorExtensions/releases/tag/1.0.1 diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArray.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArray.cs index d53c293..8b16d85 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArray.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArray.cs @@ -22,112 +22,14 @@ public sealed class EquatableImmutableArray : IEquatable Empty { get; } = new EquatableImmutableArray(ImmutableArray.Empty, 0); - // The source generation pipelines compare these a lot - // so being able to quickly tell when they are different - // is important. - // We will use an instance cache to find when we can reuse - // an existing object, massively speeding up the Equals call. - #region Instance Cache - - // The WeakReference allows the GC to collect arrays that are no longer in use. - // Thread-safe cache using dictionary of hash code -> list of arrays with that hash - private static readonly ConcurrentDictionary>>> ValueCache = new ConcurrentDictionary>>>(); - // Static factory method with singleton handling - public static EquatableImmutableArray Create(ImmutableArray values, CancellationToken cancellationToken = default) - { -#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE && !DATACUTE_EXCLUDE_GENERATORSTAGE - // Record a histogram of the array sizes we are being asked to create - LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayLength, values.Length); -#endif - if (values.IsEmpty) - return Empty; - - var hash = CalculateHashCode(values); - var list = ValueCache.GetOrAdd(hash, _ => new List>>()); - - lock (list) - { - for (int i = list.Count - 1; i >= 0; i--) - { -#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACEEXTENSIONS && !DATACUTE_EXCLUDE_GENERATORSTAGE - cancellationToken.ThrowIfCancellationRequested(0); -#else - cancellationToken.ThrowIfCancellationRequested(); -#endif - if (list[i].TryGetTarget(out var existing)) - { - if (ValuesEqual(values, existing._values)) - { - // Cache Hit -#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE && !DATACUTE_EXCLUDE_GENERATORSTAGE - LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayCacheHit); -#endif - return existing; - } - } - else - { -#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE && !DATACUTE_EXCLUDE_GENERATORSTAGE - LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayCacheWeakReferenceRemoved); -#endif - list.RemoveAt(i); - } - } - - // Cache Miss: Create a new instance -#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE && !DATACUTE_EXCLUDE_GENERATORSTAGE - LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayCacheMiss); -#endif - var newResult = new EquatableImmutableArray(values, hash); - list.Add(new WeakReference>(newResult)); - return newResult; - } - } - - private static int CalculateHashCode(ImmutableArray values) - { - var comparer = EqualityComparer.Default; - var hash = 0; - for (var index = 0; index < values.Length; index++) - { - var value = values[index]; - hash = HashHelpers_Combine(hash, value == null ? 0 : comparer.GetHashCode(value)); - } - return hash; - } - - private static int HashHelpers_Combine(int h1, int h2) - { - // RyuJIT optimizes this to use the ROL instruction - // Related GitHub pull request: https://github.com/dotnet/coreclr/pull/1830 - uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); - return ((int)rol5 + h1) ^ h2; - } - - private static bool ValuesEqual(ImmutableArray a, ImmutableArray b) - { - // Identical arrays reference check - if (a == b) return true; - - int length = a.Length; - if (length != b.Length) return false; - - var comparer = EqualityComparer.Default; - for (int i = 0; i < length; i++) - { - if (!comparer.Equals(a[i], b[i])) - return false; - } - - return true; - } - - #endregion + public static EquatableImmutableArray Create(ImmutableArray values, CancellationToken cancellationToken = default) + => EquatableImmutableArrayInstanceCache.GetOrCreate(values, cancellationToken); private readonly ImmutableArray _values; private readonly int _hashCode; private readonly int _length; + public T this[int index] => _values[index]; public int Count => _length; @@ -137,7 +39,7 @@ private static bool ValuesEqual(ImmutableArray a, ImmutableArray b) public bool IsDefault => _values.IsDefault; public bool IsDefaultOrEmpty => _values.IsDefaultOrEmpty; - private EquatableImmutableArray(ImmutableArray values, int hashCode) + internal EquatableImmutableArray(ImmutableArray values, int hashCode) { _values = values; _length = values.Length; diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayInstanceCache.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayInstanceCache.cs new file mode 100644 index 0000000..1c3e2eb --- /dev/null +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayInstanceCache.cs @@ -0,0 +1,149 @@ +// +// This file is part of the Datacute.IncrementalGeneratorExtensions package. +// It is included as a source file and should not be modified. +// + +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; + +namespace Datacute.IncrementalGeneratorExtensions +{ + // The source generation pipelines compare these a lot + // so being able to quickly tell when they are different + // is important. + // We will use an instance cache to find when we can reuse + // an existing object, massively speeding up the Equals call. + + /// + /// A cache for instances of . + /// + /// The type of elements in the array, which must implement . + public static class EquatableImmutableArrayInstanceCache where T : IEquatable + { + // Two-level cache: length -> first element hash -> list of instances + // Because this is a generic class, there is a separate static cache for each type T + private static readonly ConcurrentDictionary>>>> Cache = + new ConcurrentDictionary>>>>(); + + /// + /// Gets or creates an instance of from the provided values. + /// + /// The immutable array of values to create the instance from. + /// A cancellation token to observe while waiting for the operation to complete. + /// An instance of containing the provided values. + public static EquatableImmutableArray GetOrCreate(ImmutableArray values, CancellationToken cancellationToken = default) + { + if (values.IsEmpty) + return EquatableImmutableArray.Empty; + + // If we were to calculate the hash of the entire array first, and find + // matching instances based on that, we would still have to check each element + // for equality. Instead, we will first find a small number of potentially equal arrays, + // and then check each element for equality, since that is required anyway. + // If we find a match, we've saved the time to compute the full hash. + + // To quickly narrow down the candidates for equality checks, + // this implementation uses a two-level cache: + // 1. The first level is based on the length of the array. + // 2. The second level is based on the hash of the first element. + + // TODO: Optimize cache performance for large arrays with localized differences + // Current approach may be O(n²) when many similar large arrays differ at high indices + // Potential solutions: + // 1. Multi-element hashing (hash first few elements for better bucketing) + // 2. Sparse sampling (hash elements at strategic intervals: start, 1/4, 1/2, 3/4, end) + // 3. Adaptive difference tracking (learn common difference points, create sub-caches) + // 4. Hierarchical bucketing (first element -> second element -> etc.) + // Need real usage data to determine which approach provides best performance + + var comparer = EqualityComparer.Default; + var length = values.Length; + var firstElementHash = values[0] == null ? 0 : comparer.GetHashCode(values[0]); + + // Get or create the length-based dictionary + var lengthDict = Cache.GetOrAdd(length, _ => new ConcurrentDictionary>>>()); + + // Get or create the first-element-hash-based list + var candidateList = lengthDict.GetOrAdd(firstElementHash, _ => new List>>()); + + lock (candidateList) + { + // Check for matches (backwards to allow removal of dead references) + for (int i = candidateList.Count - 1; i >= 0; i--) + { +#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACEEXTENSIONS && !DATACUTE_EXCLUDE_GENERATORSTAGE + cancellationToken.ThrowIfCancellationRequested(0); +#else + cancellationToken.ThrowIfCancellationRequested(); +#endif + if (candidateList[i].TryGetTarget(out var existing)) + { + // Check if this candidate matches element by element + bool isMatch = true; + + for (int elementIndex = 0; elementIndex < length; elementIndex++) + { + if (!comparer.Equals(values[elementIndex], existing[elementIndex])) + { + isMatch = false; + break; + } + } + + if (isMatch) + { +#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE && !DATACUTE_EXCLUDE_GENERATORSTAGE + LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayCacheHit, values.Length); +#endif + return existing; + } + } + else + { + // Clean up dead references +#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE && !DATACUTE_EXCLUDE_GENERATORSTAGE + LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayCacheWeakReferenceRemoved, values.Length); + LightweightTrace.DecrementCount(GeneratorStage.EquatableImmutableArrayLength, values.Length); +#endif + candidateList.RemoveAt(i); + } + } + + // No match found, calculate hash and create new instance + var hash = CalculateHashCode(values, comparer, firstElementHash, 1); +#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE && !DATACUTE_EXCLUDE_GENERATORSTAGE + // Record a histogram of the array sizes we are being asked to create + LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayCacheMiss, values.Length); + LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayLength, values.Length); +#endif + var newResult = new EquatableImmutableArray(values, hash); + candidateList.Add(new WeakReference>(newResult)); + return newResult; + } + } + + private static int CalculateHashCode(ImmutableArray values, EqualityComparer comparer, int currentHash, int hashedValues) + { + int hash = currentHash; + for (var index = hashedValues; index < values.Length; index++) + { + var value = values[index]; + hash = HashHelpers_Combine(hash, value == null ? 0 : comparer.GetHashCode(value)); + } + return hash; + } + + private static int HashHelpers_Combine(int h1, int h2) + { + // RyuJIT optimizes this to use the ROL instruction + // Related GitHub pull request: https://github.com/dotnet/coreclr/pull/1830 + uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); + return ((int)rol5 + h1) ^ h2; + } + } +} +#endif \ No newline at end of file diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStage.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStage.cs index 591317a..cadb8c9 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStage.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStage.cs @@ -24,6 +24,7 @@ public enum GeneratorStage RegisterImplementationSourceOutput = 4, // PostInitializationContext Methods + PostInitializationContextAddEmbeddedAttributeDefinition = 5, PostInitializationContextAddSource = 6, // SourceProductionContext Methods diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStageDescriptions.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStageDescriptions.cs index 63e2527..b766aef 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStageDescriptions.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStageDescriptions.cs @@ -35,11 +35,12 @@ public static class GeneratorStageDescriptions { (int)GeneratorStage.Cancellation, "Operation Cancelled" }, // Output registration stages - { (int)GeneratorStage.RegisterPostInitializationOutput, "Register Post Initialization Output" }, - { (int)GeneratorStage.RegisterSourceOutput, "Register Source Output" }, - { (int)GeneratorStage.RegisterImplementationSourceOutput, "Register Implementation Source Output" }, + { (int)GeneratorStage.RegisterPostInitializationOutput, "Register Post Initialization Output - callback called" }, + { (int)GeneratorStage.RegisterSourceOutput, "Register Source Output - action called" }, + { (int)GeneratorStage.RegisterImplementationSourceOutput, "Register Implementation Source Output - action called" }, // PostInitializationContext Methods + { (int)GeneratorStage.PostInitializationContextAddEmbeddedAttributeDefinition, "Post Initialization Context Add Embedded Attribute Definition" }, { (int)GeneratorStage.PostInitializationContextAddSource, "Post Initialization Context Add Source" }, // SourceProductionContext Methods diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/SourceTextGeneratorBase.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/SourceTextGeneratorBase.cs index c0a5728..e31f2f1 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/SourceTextGeneratorBase.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/SourceTextGeneratorBase.cs @@ -18,6 +18,16 @@ namespace Datacute.IncrementalGeneratorExtensions public class SourceTextGeneratorBase where T : IEquatable { + private static readonly string _assemblyName; + private static readonly string _version; + + static SourceTextGeneratorBase() + { + var assemblyName = typeof(SourceTextGeneratorBase).Assembly.GetName(); + _assemblyName = assemblyName.Name; + _version = assemblyName.Version != null ? assemblyName.Version.ToString(fieldCount: 3) : "unknown"; + } + /// /// Initializes a new instance of the class with the provided context and cancellation token. /// @@ -79,12 +89,23 @@ protected SourceTextGeneratorBase( /// The comment that will be used at the top of the source file to indicate that this is an automatically generated file. /// protected virtual string AutoGeneratedComment => /* language=c# */ - @"//------------------------------------------------------------------------------ + $@"//------------------------------------------------------------------------------ // -// This code was generated by a source code generator. +// This code was generated by {GeneratorName}. +// Version: {Version} // //------------------------------------------------------------------------------"; + + /// + /// Gets the name of the source code generator, which will be included in the auto-generated comment. + /// + protected virtual string GeneratorName => _assemblyName ?? GetType().Namespace ?? "a source code generator"; + /// + /// Gets the version to show in the auto-generated comment. + /// + protected virtual string Version => _version; + /// /// Gets the generated source text as a object. /// diff --git a/IncrementalGeneratorExtensions/IncrementalGeneratorExtensions.csproj b/IncrementalGeneratorExtensions/IncrementalGeneratorExtensions.csproj index e1ec2e0..b76166d 100644 --- a/IncrementalGeneratorExtensions/IncrementalGeneratorExtensions.csproj +++ b/IncrementalGeneratorExtensions/IncrementalGeneratorExtensions.csproj @@ -23,11 +23,11 @@ Datacute Incremental Generator Extensions Extension methods and helper classes for incremental source generator projects. SourceGenerator source compiletime + https://github.com/datacute/IncrementalGeneratorExtensions See full release notes and changelog: $(PackageProjectUrl)/blob/main/CHANGELOG.md README.md false MIT - https://github.com/datacute/IncrementalGeneratorExtensions true $(MSBuildThisFileDirectory)../artifacts diff --git a/global.json b/global.json index f4fd385..a24b744 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,6 @@ { "sdk": { "version": "9.0.0", - "rollForward": "latestMajor", - "allowPrerelease": true + "rollForward": "latestFeature" } } \ No newline at end of file diff --git a/version.props b/version.props index 39ae365..8f3c87d 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 1.0.3 + 1.0.4