diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index b45d6a9..f313729 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -175,7 +175,7 @@ public static class Program } // Wait for tiered JIT background compilation to complete - Thread.Sleep(2000); + Thread.Sleep(3000); // Run benchmarks System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n"); diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index d65ed3e..81544b1 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -114,6 +114,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator bool hasGenWriter = false; bool propTypeIsIId = false; bool propEnableMetadata = true; + bool childNeedsIdScan = true; + bool childNeedsAllRefScan = true; + bool childNeedsInternScan = true; string? writerClassName = null; string? propIdTypeName = null; int childTypeNameHash = 0; @@ -133,6 +136,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator { // Read child type's EnableMetadataFeature propEnableMetadata = ReadEnableMetadata(resolvedType); + var childScanFlags = ComputeNeedsScan(resolvedType); + childNeedsIdScan = childScanFlags.needsIdScan; + childNeedsAllRefScan = childScanFlags.needsAllRefScan; + childNeedsInternScan = childScanFlags.needsInternScan; var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i => i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); @@ -160,6 +167,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator bool elemHasGenWriter = false; bool elemIsIId = false; bool elemEnableMetadata = true; + bool elemNeedsIdScan = true; + bool elemNeedsAllRefScan = true; + bool elemNeedsInternScan = true; string? elemWriterClassName = null; string? elemIdTypeName = null; string? collKind = null; @@ -206,6 +216,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator { // Read element type's EnableMetadataFeature elemEnableMetadata = ReadEnableMetadata(resolvedElem); + var elemScanFlags = ComputeNeedsScan(resolvedElem); + elemNeedsIdScan = elemScanFlags.needsIdScan; + elemNeedsAllRefScan = elemScanFlags.needsAllRefScan; + elemNeedsInternScan = elemScanFlags.needsInternScan; var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc => ifc.IsGenericType && ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); @@ -238,7 +252,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName, childTypeNameHash, childPropertyHashes, elementTypeNameHash, elementPropertyHashes, - propEnableMetadata, elemEnableMetadata)); + propEnableMetadata, elemEnableMetadata, + childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan, + elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan)); } } @@ -263,7 +279,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator var className = BuildFlatName(typeSymbol); var typeNameHash = ComputeFnvHash(typeSymbol.Name); var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray(); - return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata); + var selfScanFlags = ComputeNeedsScan(typeSymbol); + return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan); } private static void Execute(ImmutableArray classes, SourceProductionContext context) @@ -343,6 +360,25 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine(" public void ScanObject(object value, AcBinarySerializer.BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase"); sb.AppendLine(" {"); + // Compile-time proven: no scan work needed for this type + if (!ci.NeedsScan) + { + sb.AppendLine(" // NeedsScan=false: no ref tracking, no string interning, no scannable children"); + sb.AppendLine(" }"); + return; + } + + // Early return: skip scan when no active runtime feature matches this type's needs + if (!ci.NeedsIdScan) + { + if (ci.NeedsAllRefScan && ci.NeedsInternScan) + sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.All && !context.UseStringInterning) return;"); + else if (ci.NeedsAllRefScan) + sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.All) return;"); + else if (ci.NeedsInternScan) + sb.AppendLine(" if (!context.UseStringInterning) return;"); + } + // Null/depth guard — matches runtime ScanValue entry sb.AppendLine(" if (value == null || depth > context.MaxDepth) return;"); sb.AppendLine(); @@ -634,13 +670,40 @@ public class AcBinarySourceGenerator : IIncrementalGenerator /// private static void EmitScanComplexSGen(StringBuilder sb, PropInfo p, string a, string i) { + // Compile-time proven: child scan is no-op — skip entirely + if (!p.ChildNeedsScan) return; + var writer = p.WriterClassName; var childVar = $"sc_{p.Name}"; - // Null check only — ScanObject handles depth + ref tracking internally - sb.AppendLine($"{i}var {childVar} = {a};"); - sb.AppendLine($"{i}if ({childVar} != null)"); - sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context, depth + 1);"); + // 3-axis guard: IId → always call, AllRef → guard All mode, Intern → guard UseStringInterning + string? guard = null; + if (!p.ChildNeedsIdScan) + { + if (p.ChildNeedsAllRefScan && p.ChildNeedsInternScan) + guard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; + else if (p.ChildNeedsAllRefScan) + guard = "context.ReferenceHandling == ReferenceHandlingMode.All"; + else if (p.ChildNeedsInternScan) + guard = "context.UseStringInterning"; + } + + if (guard != null) + { + sb.AppendLine($"{i}if ({guard})"); + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} var {childVar} = {a};"); + sb.AppendLine($"{i} if ({childVar} != null)"); + sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context, depth + 1);"); + sb.AppendLine($"{i}}}"); + } + else + { + // IId in subtree — always call (active in OnlyId + All) + sb.AppendLine($"{i}var {childVar} = {a};"); + sb.AppendLine($"{i}if ({childVar} != null)"); + sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context, depth + 1);"); + } } /// @@ -701,12 +764,32 @@ public class AcBinarySourceGenerator : IIncrementalGenerator // Complex element collection with SGen writer if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null) { + // Compile-time proven: element scan is no-op — skip entirely + if (!p.ElementNeedsScan) return; + var writer = p.ElementWriterClassName; - sb.AppendLine($"{i}var scol_{p.Name} = {a};"); - sb.AppendLine($"{i}if (scol_{p.Name} != null)"); + // 3-axis guard: IId → always scan, AllRef → guard All mode, Intern → guard UseStringInterning + string? elemGuard = null; + if (!p.ElementNeedsIdScan) + { + if (p.ElementNeedsAllRefScan && p.ElementNeedsInternScan) + elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; + else if (p.ElementNeedsAllRefScan) + elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All"; + else if (p.ElementNeedsInternScan) + elemGuard = "context.UseStringInterning"; + } + + // Guard entire collection scan with runtime check when no IId in element subtree + if (elemGuard != null) + sb.AppendLine($"{i}if ({elemGuard})"); + sb.AppendLine($"{i}{{"); - sb.AppendLine($"{i} var snd_{p.Name} = depth + 1;"); + sb.AppendLine($"{i} var scol_{p.Name} = {a};"); + sb.AppendLine($"{i} if (scol_{p.Name} != null)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} var snd_{p.Name} = depth + 1;"); if (p.CollectionKind == "Array") { @@ -731,6 +814,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} if ({e} == null) continue;"); sb.AppendLine($"{i} {writer}.Instance.ScanObject({e}, context, snd_{p.Name});"); sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); return; } @@ -1168,6 +1252,116 @@ public class AcBinarySourceGenerator : IIncrementalGenerator return true; } + /// + /// Computes whether a type needs scan pass work, split into ref tracking and string interning. + /// Uses a per-call HashSet to guard against circular references (no static cache — + /// static state is unsafe in incremental generators as it persists across builds). + /// Returns (needsRefScan, needsInternScan) — these are independent axes. + /// + private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScan(ITypeSymbol type) + { + return ComputeNeedsScanCore(type, new HashSet()); + } + + private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScanCore(ITypeSymbol type, HashSet visiting) + { + // Circular reference guard: if already visiting this type, assume true (safe fallback) + var key = type.ToDisplayString(); + if (!visiting.Add(key)) + return (true, true, true); + + // Read [AcBinarySerializable] flags + var attr = type.GetAttributes().FirstOrDefault(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + + bool enableIdTracking = true, enableRefHandling = true, enableInternString = true; + if (attr != null) + { + if (attr.ConstructorArguments.Length == 1) + { + var all = (bool)attr.ConstructorArguments[0].Value!; + enableIdTracking = enableRefHandling = enableInternString = all; + } + else if (attr.ConstructorArguments.Length == 4) + { + enableIdTracking = (bool)attr.ConstructorArguments[1].Value!; + enableRefHandling = (bool)attr.ConstructorArguments[2].Value!; + enableInternString = (bool)attr.ConstructorArguments[3].Value!; + } + } + + // IId tracking: active in OnlyId + All modes + var isIId = enableIdTracking && type.AllInterfaces.Any(i => + i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + var needsIdScan = isIId; + // Non-IId ref tracking: active only in All mode + var needsAllRefScan = !isIId && enableRefHandling; + var needsInternScan = false; + + // Check properties for string interning or complex children + foreach (var member in type.GetMembers()) + { + if (member is not IPropertySymbol p || + p.DeclaredAccessibility != Accessibility.Public || + p.GetMethod == null || p.SetMethod == null || + p.IsIndexer || p.IsStatic) + continue; + + var hasIgnore = p.GetAttributes().Any(a => + { + var name = a.AttributeClass?.Name ?? ""; + return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; + }); + if (hasIgnore) continue; + + // Early exit: if all flags are already true, no need to check more properties + if (needsIdScan && needsAllRefScan && needsInternScan) break; + + var kind = GetKind(p.Type); + + // String with interning? + if (enableInternString && kind == PropertyTypeKind.String) + { + var internAttr = p.GetAttributes().FirstOrDefault(a => + a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute"); + if (internAttr == null || (internAttr.ConstructorArguments.Length == 1 && (bool)internAttr.ConstructorArguments[0].Value!)) + needsInternScan = true; + } + + // Complex child → recurse + if (kind == PropertyTypeKind.Complex) + { + var resolved = p.Type is INamedTypeSymbol nt ? nt.OriginalDefinition : p.Type; + var childFlags = ComputeNeedsScanCore(resolved, visiting); + needsIdScan |= childFlags.needsIdScan; + needsAllRefScan |= childFlags.needsAllRefScan; + needsInternScan |= childFlags.needsInternScan; + } + + // Collection → check element type + if (kind == PropertyTypeKind.Collection) + { + var elemType = GetCollectionElementType(p.Type); + if (elemType != null) + { + var elemKind = GetKind(elemType); + if (enableInternString && elemKind == PropertyTypeKind.String) + needsInternScan = true; + if (elemKind == PropertyTypeKind.Complex) + { + var resolvedElem = elemType is INamedTypeSymbol ne ? ne.OriginalDefinition : elemType; + var elemFlags = ComputeNeedsScanCore(resolvedElem, visiting); + needsIdScan |= elemFlags.needsIdScan; + needsAllRefScan |= elemFlags.needsAllRefScan; + needsInternScan |= elemFlags.needsInternScan; + } + } + } + } + + return (needsIdScan, needsAllRefScan, needsInternScan); + } + #region FNV-1a Hash (compile-time) private static int ComputeFnvHash(string value) @@ -1315,8 +1509,18 @@ internal sealed class SerializableClassInfo public int[] PropertyNameHashes { get; } /// When false, skip inline metadata and use markerless property write for this type. public bool EnableMetadata { get; } - public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata) - { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; } + /// When true, type subtree has IId types needing scan (active in OnlyId + All). + public bool NeedsIdScan { get; } + /// When true, type subtree has non-IId ref tracking (active only in All mode). + public bool NeedsAllRefScan { get; } + /// When true, type subtree needs string interning scan. + public bool NeedsInternScan { get; } + /// Derived: NeedsIdScan || NeedsAllRefScan. + public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan; + /// Derived: any scan axis active. + public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan; + public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool needsIdScan, bool needsAllRefScan, bool needsInternScan) + { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; } } internal sealed class PropInfo @@ -1375,6 +1579,26 @@ internal sealed class PropInfo public bool ChildEnableMetadata { get; } /// When false, collection element type skips inline metadata in generated code. public bool ElementEnableMetadata { get; } + /// When true, child subtree has IId types needing scan (active in OnlyId + All). + public bool ChildNeedsIdScan { get; } + /// When true, child subtree has non-IId ref tracking (active only in All mode). + public bool ChildNeedsAllRefScan { get; } + /// When true, child subtree needs string interning scan. + public bool ChildNeedsInternScan { get; } + /// Derived: ChildNeedsIdScan || ChildNeedsAllRefScan. + public bool ChildNeedsRefScan => ChildNeedsIdScan || ChildNeedsAllRefScan; + /// Derived: any child scan axis active. + public bool ChildNeedsScan => ChildNeedsIdScan || ChildNeedsAllRefScan || ChildNeedsInternScan; + /// When true, element subtree has IId types needing scan (active in OnlyId + All). + public bool ElementNeedsIdScan { get; } + /// When true, element subtree has non-IId ref tracking (active only in All mode). + public bool ElementNeedsAllRefScan { get; } + /// When true, element subtree needs string interning scan. + public bool ElementNeedsInternScan { get; } + /// Derived: ElementNeedsIdScan || ElementNeedsAllRefScan. + public bool ElementNeedsRefScan => ElementNeedsIdScan || ElementNeedsAllRefScan; + /// Derived: any element scan axis active. + public bool ElementNeedsScan => ElementNeedsIdScan || ElementNeedsAllRefScan || ElementNeedsInternScan; public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable, bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, string? idTypeName = null, @@ -1382,7 +1606,9 @@ internal sealed class PropInfo string? elementWriterClassName = null, string? elementIdTypeName = null, string? collectionKind = null, string? elementFullTypeName = null, int childTypeNameHash = 0, int[]? childPropertyHashes = null, int elementTypeNameHash = 0, int[]? elementPropertyHashes = null, - bool childEnableMetadata = true, bool elementEnableMetadata = true) + bool childEnableMetadata = true, bool elementEnableMetadata = true, + bool childNeedsIdScan = true, bool childNeedsAllRefScan = true, bool childNeedsInternScan = true, + bool elementNeedsIdScan = true, bool elementNeedsAllRefScan = true, bool elementNeedsInternScan = true) { Name = n; TypeName = tn; @@ -1406,6 +1632,12 @@ internal sealed class PropInfo ElementPropertyHashes = elementPropertyHashes; ChildEnableMetadata = childEnableMetadata; ElementEnableMetadata = elementEnableMetadata; + ChildNeedsIdScan = childNeedsIdScan; + ChildNeedsAllRefScan = childNeedsAllRefScan; + ChildNeedsInternScan = childNeedsInternScan; + ElementNeedsIdScan = elementNeedsIdScan; + ElementNeedsAllRefScan = elementNeedsAllRefScan; + ElementNeedsInternScan = elementNeedsInternScan; // Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase int flags = 0; if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index e0fff73..6b01f75 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// allowing the deserializer to match properties by name between different types. /// Default: false (no overhead) /// - public bool UseMetadata { get; set; } = true; + public bool UseMetadata { get; set; } = false; public bool UseGeneratedCode { get; set; } = true;