[LOADED_DOCS: 2 files, no new loads]

Enable per-type property filter opt-out in AcBinary

Adds EnablePropertyFilterFeature to AcBinarySerializableAttribute, allowing types to opt out of property filter codegen and runtime checks. Updates source generator, metadata, and runtime logic to honor this flag. Removes UsePropertyFilter constant; emission is now attribute-driven. Also optimizes string serialization for non-ASCII cases and refactors deserializer byte reads for trusted single-segment fast paths. Backward compatible: property filter remains enabled by default.
This commit is contained in:
Loretta 2026-05-10 19:01:30 +02:00
parent eb4b6e7f8f
commit 96c09a65bb
7 changed files with 162 additions and 82 deletions

View File

@ -124,10 +124,10 @@ public static class Program
/// Aggregation rule: if ALL tagged types have the feature enabled → <c>true</c>; if any tagged type /// Aggregation rule: if ALL tagged types have the feature enabled → <c>true</c>; if any tagged type
/// disables it → <c>false</c> (a single disabling type suppresses the feature on the type-graph). /// disables it → <c>false</c> (a single disabling type suppresses the feature on the type-graph).
/// </summary> /// </summary>
private static readonly (bool refHandling, bool internString, bool metadata, bool idTracking) _attrFlags private static readonly (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) _attrFlags
= ScanAcBinaryAttributeFlags(); = ScanAcBinaryAttributeFlags();
private static (bool refHandling, bool internString, bool metadata, bool idTracking) ScanAcBinaryAttributeFlags() private static (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) ScanAcBinaryAttributeFlags()
{ {
var attrs = AppDomain.CurrentDomain.GetAssemblies() var attrs = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty<Type>(); } }) .SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty<Type>(); } })
@ -135,13 +135,14 @@ public static class Program
.Where(a => a != null) .Where(a => a != null)
.ToList(); .ToList();
if (attrs.Count == 0) return (false, false, false, false); if (attrs.Count == 0) return (false, false, false, false, false);
return ( return (
refHandling: attrs.All(a => a!.EnableRefHandlingFeature), refHandling: attrs.All(a => a!.EnableRefHandlingFeature),
internString: attrs.All(a => a!.EnableInternStringFeature), internString: attrs.All(a => a!.EnableInternStringFeature),
metadata: attrs.All(a => a!.EnableMetadataFeature), metadata: attrs.All(a => a!.EnableMetadataFeature),
idTracking: attrs.All(a => a!.EnableIdTrackingFeature)); idTracking: attrs.All(a => a!.EnableIdTrackingFeature),
propertyFilter: attrs.All(a => a!.EnablePropertyFilterFeature));
} }
/// <summary> /// <summary>
@ -153,10 +154,16 @@ public static class Program
/// </summary> /// </summary>
private static string BuildAcBinaryOptionsDescription(AcBinarySerializerOptions options, string extra = "") private static string BuildAcBinaryOptionsDescription(AcBinarySerializerOptions options, string extra = "")
{ {
// PropertyFilter: opt-side is "Set"/"None" depending on whether a callback is registered (the callback
// itself isn't a meaningful display value); attr-side is the cross-type-aggregated bool (true = every
// tagged type has the feature enabled, false = at least one type opted out via
// [AcBinarySerializable(enablePropertyFilterFeature: false)] → SGen-emit + Runtime hot-loop both gate).
var propFilterOpt = options.PropertyFilter == null ? "None" : "Set";
return $"WireMode={options.WireMode}, " + return $"WireMode={options.WireMode}, " +
$"RefHandling={options.ReferenceHandling}(opt) | {_attrFlags.refHandling} (attr), " + $"RefHandling={options.ReferenceHandling}(opt) | {_attrFlags.refHandling} (attr), " +
$"Interning={options.UseStringInterning}(opt) | {_attrFlags.internString} (attr), " + $"Interning={options.UseStringInterning}(opt) | {_attrFlags.internString} (attr), " +
$"Metadata={options.UseMetadata}(opt) | {_attrFlags.metadata} (attr), " + $"Metadata={options.UseMetadata}(opt) | {_attrFlags.metadata} (attr), " +
$"PropertyFilter={propFilterOpt}(opt) | {_attrFlags.propertyFilter} (attr), " +
$"SGen={options.UseGeneratedCode}, " + $"SGen={options.UseGeneratedCode}, " +
$"Compression={options.UseCompression}{extra}"; $"Compression={options.UseCompression}{extra}";
} }

View File

@ -41,7 +41,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// attribute properties so consumers can opt out per type. Until then, keep both `false` for // attribute properties so consumers can opt out per type. Until then, keep both `false` for
// benchmark-vs-MemPack measurements; flip to `true` for production where the features are needed. // benchmark-vs-MemPack measurements; flip to `true` for production where the features are needed.
// ──────────────────────────────────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────────────────────────────────
private const bool UsePropertyFilter = false; // UsePropertyFilter const removed — replaced by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]`
// attribute flag, propagated through SerializableClassInfo.EnablePropertyFilter to EmitProp/EmitScanProp.
private const bool UsePolymorphType = false; private const bool UsePolymorphType = false;
private static readonly DiagnosticDescriptor CircularReferenceWarning = new( private static readonly DiagnosticDescriptor CircularReferenceWarning = new(
@ -82,6 +83,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var enableRefHandling = true; var enableRefHandling = true;
var enableInternString = true; var enableInternString = true;
var enableMetadata = true; var enableMetadata = true;
var enablePropertyFilter = true;
var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a => var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a =>
a.AttributeClass?.ToDisplayString() == AttributeName); a.AttributeClass?.ToDisplayString() == AttributeName);
if (binarySerializableAttr != null) if (binarySerializableAttr != null)
@ -94,15 +96,25 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
enableRefHandling = all; enableRefHandling = all;
enableInternString = all; enableInternString = all;
enableMetadata = all; enableMetadata = all;
enablePropertyFilter = all;
} }
else if (binarySerializableAttr.ConstructorArguments.Length == 4) else if (binarySerializableAttr.ConstructorArguments.Length == 4)
{ {
// Four bool ctor: (metadata, idTracking, refHandling, internString) // Four bool ctor: (metadata, idTracking, refHandling, internString) — filter defaults to true
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
} }
else if (binarySerializableAttr.ConstructorArguments.Length == 5)
{
// Five bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter)
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!;
}
} }
foreach (var p in GetAllSerializablePropertySymbols(typeSymbol)) foreach (var p in GetAllSerializablePropertySymbols(typeSymbol))
@ -384,7 +396,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var typeNameHash = ComputeFnvHash(typeSymbol.Name); var typeNameHash = ComputeFnvHash(typeSymbol.Name);
var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray(); var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray();
var selfScanFlags = ComputeNeedsScan(typeSymbol); var selfScanFlags = ComputeNeedsScan(typeSymbol);
return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan); return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, enablePropertyFilter, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan);
} }
private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context) private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context)
@ -537,7 +549,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
foreach (var p in ci.Properties) foreach (var p in ci.Properties)
{ {
sb.AppendLine(); sb.AppendLine();
EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata); EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata, ci.EnablePropertyFilter);
} }
sb.AppendLine(" }"); sb.AppendLine(" }");
@ -668,7 +680,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
{ {
sb.AppendLine(); sb.AppendLine();
hasAnyScanProp = true; hasAnyScanProp = true;
EmitScanProp(sb, p, " ", ci.FullTypeName); EmitScanProp(sb, p, " ", ci.FullTypeName, ci.EnablePropertyFilter);
} }
if (!hasAnyScanProp) if (!hasAnyScanProp)
@ -679,7 +691,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine(" }"); sb.AppendLine(" }");
} }
private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata) private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata, bool enablePropertyFilter)
{ {
var a = $"obj.{p.Name}"; var a = $"obj.{p.Name}";
@ -699,9 +711,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// All non-markerless properties: emit PropertyFilter guard // All non-markerless properties: emit PropertyFilter guard
// When filter returns false, write PropertySkip and skip the property write. // When filter returns false, write PropertySkip and skip the property write.
// Gated by `UsePropertyFilter` (TEMPORARY const) — `false` skips emit entirely → leaner // Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — when false, the entire
// generated code on benchmark workloads where no property-filter is ever set. // filter-check block is omitted from emit (no `HasPropertyFilter` test, no filter-context allocation,
if (UsePropertyFilter) // no lambda-call) → leaner generated code on hot-path types that never use a property-filter.
if (enablePropertyFilter)
{ {
sb.AppendLine($"{i}if (context.HasPropertyFilter)"); sb.AppendLine($"{i}if (context.HasPropertyFilter)");
sb.AppendLine($"{i}{{"); sb.AppendLine($"{i}{{");
@ -911,7 +924,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
/// Complex (no SGen): fallback to ScanValueGenerated (runtime wrapper lookup). /// Complex (no SGen): fallback to ScanValueGenerated (runtime wrapper lookup).
/// Collection: iterate elements with same patterns. /// Collection: iterate elements with same patterns.
/// </summary> /// </summary>
private static void EmitScanProp(StringBuilder sb, PropInfo p, string i, string fullTypeName) private static void EmitScanProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enablePropertyFilter)
{ {
// Compile-time proven: no scan work for this property — skip entirely (including PropertyFilter guard) // Compile-time proven: no scan work for this property — skip entirely (including PropertyFilter guard)
if (!HasScanWork(p)) return; if (!HasScanWork(p)) return;
@ -920,8 +933,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// PropertyFilter: must match write pass — if filter skips property, scan must skip too // PropertyFilter: must match write pass — if filter skips property, scan must skip too
// Only for non-markerless properties (matching EmitProp behavior). // Only for non-markerless properties (matching EmitProp behavior).
// Gated by `UsePropertyFilter` (TEMPORARY const) — same A/B flag as the writer pass. // Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — same gate as the writer pass.
if (UsePropertyFilter && !IsMarkerless(p.TypeKind)) if (enablePropertyFilter && !IsMarkerless(p.TypeKind))
{ {
sb.AppendLine($"{i}if (context.HasPropertyFilter)"); sb.AppendLine($"{i}if (context.HasPropertyFilter)");
sb.AppendLine($"{i}{{"); sb.AppendLine($"{i}{{");
@ -2996,6 +3009,10 @@ internal sealed class SerializableClassInfo
public int[] PropertyNameHashes { get; } public int[] PropertyNameHashes { get; }
/// <summary>When false, skip inline metadata and use markerless property write for this type.</summary> /// <summary>When false, skip inline metadata and use markerless property write for this type.</summary>
public bool EnableMetadata { get; } public bool EnableMetadata { get; }
/// <summary>True if EnablePropertyFilterFeature is enabled — controls per-property HasPropertyFilter
/// guard emission in WriteProperties / ScanObject. When false, the filter check is omitted entirely
/// → leaner generated code on the hot path (typical for high-throughput types that never use a filter).</summary>
public bool EnablePropertyFilter { get; }
/// <summary>When true, type subtree has IId types needing scan (active in OnlyId + All).</summary> /// <summary>When true, type subtree has IId types needing scan (active in OnlyId + All).</summary>
public bool NeedsIdScan { get; } public bool NeedsIdScan { get; }
/// <summary>When true, type subtree has non-IId ref tracking (active only in All mode).</summary> /// <summary>When true, type subtree has non-IId ref tracking (active only in All mode).</summary>
@ -3006,8 +3023,8 @@ internal sealed class SerializableClassInfo
public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan; public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan;
/// <summary>Derived: any scan axis active.</summary> /// <summary>Derived: any scan axis active.</summary>
public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan; public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan;
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool needsIdScan, bool needsAllRefScan, bool needsInternScan) public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool enablePropertyFilter, 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; } { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; EnablePropertyFilter = enablePropertyFilter; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; }
} }
internal sealed class PropInfo internal sealed class PropInfo

View File

@ -19,6 +19,19 @@ public sealed class AcBinarySerializableAttribute : Attribute
public bool EnableRefHandlingFeature { get; } public bool EnableRefHandlingFeature { get; }
public bool EnableInternStringFeature { get; } public bool EnableInternStringFeature { get; }
/// <summary>
/// When <c>true</c> (default): the SGen-emitted writer/scan code includes the per-property
/// <c>HasPropertyFilter</c> branch + filter-context allocation + lambda-call. Required for runtime
/// <see cref="AyCode.Core.Serializers.Binaries.BinaryPropertyFilter"/> to take effect on this type.
/// <para>When <c>false</c>: the SGen-emitted writer/scan code <b>omits</b> the filter check entirely
/// for every property of this type — leaner generated code on the hot path. Use this when the
/// type is never serialized with a property-filter (typical for high-throughput message types).
/// Filter callbacks set on <c>AcBinarySerializerOptions.PropertyFilter</c> will be silently ignored
/// for this type's properties — only set <c>false</c> if you can guarantee no consumer relies on
/// per-property filtering for this specific type.</para>
/// </summary>
public bool EnablePropertyFilterFeature { get; }
public AcBinarySerializableAttribute() : this(true) public AcBinarySerializableAttribute() : this(true)
{ {
} }
@ -29,13 +42,15 @@ public sealed class AcBinarySerializableAttribute : Attribute
EnableIdTrackingFeature = enableAllFeatures; EnableIdTrackingFeature = enableAllFeatures;
EnableRefHandlingFeature = enableAllFeatures; EnableRefHandlingFeature = enableAllFeatures;
EnableInternStringFeature = enableAllFeatures; EnableInternStringFeature = enableAllFeatures;
EnablePropertyFilterFeature = enableAllFeatures;
} }
public AcBinarySerializableAttribute(bool enableMetadataFeature, bool enableIdTrackingFeature, bool enableRefHandlingFeature, bool enableInternStringFeature) public AcBinarySerializableAttribute(bool enableMetadataFeature, bool enableIdTrackingFeature, bool enableRefHandlingFeature, bool enableInternStringFeature, bool enablePropertyFilterFeature)
{ {
EnableMetadataFeature = enableMetadataFeature; EnableMetadataFeature = enableMetadataFeature;
EnableIdTrackingFeature = enableIdTrackingFeature; EnableIdTrackingFeature = enableIdTrackingFeature;
EnableRefHandlingFeature = enableRefHandlingFeature; EnableRefHandlingFeature = enableRefHandlingFeature;
EnableInternStringFeature = enableInternStringFeature; EnableInternStringFeature = enableInternStringFeature;
EnablePropertyFilterFeature = enablePropertyFilterFeature;
} }
} }

View File

@ -30,8 +30,10 @@ public static partial class AcBinaryDeserializer
get get
{ {
if (_position < _bufferLength) return false; if (_position < _bufferLength) return false;
// Multi-segment: try advancing to next segment before declaring end. // Trusted-single-segment fast path — JIT folds the constant: for ArrayBinaryInput
// ArrayBinaryInput: TryAdvanceSegment => false (JIT eliminates, same as before). // the branch becomes `if (true) return true;` and the TryAdvanceSegment call below is
// dead-code-eliminated. For multi-segment / streaming inputs, the call is preserved.
if (TInput.IsTrustedSingleSegment) return true;
return !Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1); return !Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1);
} }
} }
@ -47,27 +49,18 @@ public static partial class AcBinaryDeserializer
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte ReadByte() public byte ReadByte()
{ {
if (_position >= _bufferLength) // Routes through EnsureAvailable to leverage the trusted-single-segment JIT-eliminate guard.
{ // ArrayBinaryInput: EnsureAvailable body fully eliminated → just `_buffer[_position++]`.
// Multi-segment: try advancing to next segment before giving up. // Multi-segment / streaming: bounds-check + TryAdvanceSegment kept (cross-segment safe).
// ArrayBinaryInput: TryAdvanceSegment => false (JIT eliminates this branch). EnsureAvailable(1);
if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1))
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
}
return _buffer[_position++]; return _buffer[_position++];
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte PeekByte() public byte PeekByte()
{ {
if (_position >= _bufferLength) // Same trusted-single-segment fast path as ReadByte (no _position advance).
{ EnsureAvailable(1);
// Multi-segment: try advancing to next segment before giving up.
if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1))
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
}
return _buffer[_position]; return _buffer[_position];
} }

View File

@ -830,57 +830,86 @@ public static partial class AcBinarySerializer
WriteVarUIntUnsafe((uint)bytesWritten); WriteVarUIntUnsafe((uint)bytesWritten);
_position += bytesWritten; _position += bytesWritten;
} }
return;
} }
else
switch (bytesWritten)
{ {
// Non-ASCII — post-encode tier choice from bytesWritten (smallest fitting tier wins) // Non-ASCII — post-encode tier choice (smallest fitting tier wins). One if-else chain
int actualHeader; // per tier; each branch handles shift + header-store + position update inline.
byte tierMarker; //
switch (bytesWritten) // Combined header-store optimization (shift > 0 only):
// When the actual tier downgrades from the predicted reserve (e.g. Medium predicted
// from charLength but Small actual from bytesWritten), the body needs a left-shift
// memcpy. We write the FULL combined header (uint for Small, ulong for Medium) at
// savedPos BEFORE the body memcpy — the slack byte(s) at the high end of the store
// get overwritten by the subsequent memcpy → 1 store instead of 1+N for the header.
// When shift == 0 (predicted tier matches actual), the body is already at its final
// position; a combined store would corrupt the body's first byte(s), so we fall
// back to separate marker + packed-header stores.
//
// Big tier (9-byte header) always has shift == 0 (reserveHeader == actualHeader == 9)
// because Big is the largest tier — no downgrade path possible. The 1-byte marker +
// 8-byte ulong-packed pattern is already minimal (no slack issue, 9 dedicated bytes).
case <= 255:
{ {
case <= 255: // Small tier: 3-byte header [marker:1][charLen:8][utf8Len:8]
actualHeader = 3; var shift = reserveHeader - 3;
tierMarker = BinaryTypeCode.StringSmall; if (shift > 0)
break; {
case <= 65535: // Combined uint store: 4 bytes physical, 3 bytes meaningful, 1 byte slack
actualHeader = 5; // at savedPos+3 — overwritten by body memcpy below.
tierMarker = BinaryTypeCode.StringMedium; var packedFull = (uint)BinaryTypeCode.StringSmall
break; | ((uint)charLength << 8)
default: | ((uint)bytesWritten << 16);
actualHeader = 9; Unsafe.WriteUnaligned<uint>(ref _buffer[savedPos], packedFull);
tierMarker = BinaryTypeCode.StringBig; _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(savedPos + 3, bytesWritten));
break; }
else
{
// shift == 0: body in place at savedPos+3 = encodeStart. Combined uint store
// would corrupt body's first byte; use separate 1-byte marker + 2-byte packed.
_buffer[savedPos] = BinaryTypeCode.StringSmall;
var packedHl = (ushort)(charLength | (bytesWritten << 8));
Unsafe.WriteUnaligned<ushort>(ref _buffer[savedPos + 1], packedHl);
}
_position = savedPos + 3 + bytesWritten;
break;
} }
case <= 65535:
var nonAsciiShift = reserveHeader - actualHeader;
if (nonAsciiShift > 0) _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(encodeStart - nonAsciiShift, bytesWritten));
_buffer[savedPos] = tierMarker;
switch (actualHeader)
{ {
case 3: // Medium tier: 5-byte header [marker:1][charLen:16][utf8Len:16]
var shift = reserveHeader - 5;
if (shift > 0)
{ {
// A-direction: pack charLen:8 | utf8Len:8 → single ushort store // Combined ulong store: 8 bytes physical, 5 bytes meaningful, 3 bytes slack
var packed = (ushort)(charLength | (bytesWritten << 8)); // at savedPos+5..7 — overwritten by body memcpy below.
Unsafe.WriteUnaligned<ushort>(ref _buffer[savedPos + 1], packed); var packedFull = (ulong)BinaryTypeCode.StringMedium
break; | ((ulong)(uint)charLength << 8)
| ((ulong)(uint)bytesWritten << 24);
Unsafe.WriteUnaligned<ulong>(ref _buffer[savedPos], packedFull);
_buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(savedPos + 5, bytesWritten));
} }
case 5: else
{ {
// A-direction: pack charLen:16 | utf8Len:16 → single uint store, LE // shift == 0: separate 1-byte marker + 4-byte packed.
var packed = (uint)charLength | ((uint)bytesWritten << 16); _buffer[savedPos] = BinaryTypeCode.StringMedium;
Unsafe.WriteUnaligned<uint>(ref _buffer[savedPos + 1], packed); var packedHl = (uint)charLength | ((uint)bytesWritten << 16);
break; Unsafe.WriteUnaligned<uint>(ref _buffer[savedPos + 1], packedHl);
}
default:
{
// A-direction: pack charLen:32 | utf8Len:32 → single ulong store, LE
var packed = (ulong)(uint)charLength | ((ulong)(uint)bytesWritten << 32);
Unsafe.WriteUnaligned<ulong>(ref _buffer[savedPos + 1], packed);
break;
} }
_position = savedPos + 5 + bytesWritten;
break;
}
default:
{
// Big tier: 9-byte header [marker:1][charLen:32][utf8Len:32]. shift always 0.
_buffer[savedPos] = BinaryTypeCode.StringBig;
var packed = (ulong)(uint)charLength | ((ulong)(uint)bytesWritten << 32);
Unsafe.WriteUnaligned<ulong>(ref _buffer[savedPos + 1], packed);
_position = savedPos + 9 + bytesWritten;
break;
} }
_position = savedPos + actualHeader + bytesWritten;
} }
} }

View File

@ -1663,7 +1663,11 @@ public static partial class AcBinarySerializer
{ {
var properties = wrapper.Metadata.Properties; var properties = wrapper.Metadata.Properties;
var propCount = properties.Length; var propCount = properties.Length;
var hasPropertyFilter = context.HasPropertyFilter; // Combined runtime+attribute gate: if the type opted out of the filter feature via
// [AcBinarySerializable(EnablePropertyFilterFeature = false)], the runtime PropertyFilter is
// ignored for this type — short-circuit eliminates the per-property `ShouldSerializeProperty`
// call entirely. Mirrors the SGen-side gate in `EmitProp`.
var hasPropertyFilter = context.HasPropertyFilter && wrapper.Metadata.EnablePropertyFilterFeature;
for (var i = 0; i < propCount; i++) for (var i = 0; i < propCount; i++)
{ {
@ -1684,7 +1688,8 @@ public static partial class AcBinarySerializer
{ {
var properties = wrapper.Metadata.Properties; var properties = wrapper.Metadata.Properties;
var propCount = properties.Length; var propCount = properties.Length;
var hasPropertyFilter = context.HasPropertyFilter; // Combined runtime+attribute gate — see WritePropertiesWithMeta for the rationale.
var hasPropertyFilter = context.HasPropertyFilter && wrapper.Metadata.EnablePropertyFilterFeature;
for (var i = 0; i < propCount; i++) for (var i = 0; i < propCount; i++)
{ {

View File

@ -138,6 +138,19 @@ public abstract class TypeMetadataBase
/// </summary> /// </summary>
public bool EnableMetadataFeature { get; } public bool EnableMetadataFeature { get; }
/// <summary>
/// When false, this type's runtime write loop skips the per-property
/// <c>PropertyFilter</c> callback check entirely — runtime-set filters on
/// <see cref="AyCode.Core.Serializers.Binaries.AcBinarySerializerOptions.PropertyFilter"/>
/// are silently ignored for this type.
/// <para>Read from [AcBinarySerializable(EnablePropertyFilterFeature = ...)]; defaults to true.
/// Mirrors the SGen-side gate in <c>AcBinarySourceGenerator.EmitProp</c>: when false, the SGen
/// emit omits the filter-check block entirely. Runtime-side this flag combines with
/// <c>context.HasPropertyFilter</c> via short-circuit AND to drop the per-property branch
/// for filter-disabled types.</para>
/// </summary>
public bool EnablePropertyFilterFeature { get; }
/// <summary> /// <summary>
/// True if this type is a primitive, string, enum, Guid, DateTime, etc. /// True if this type is a primitive, string, enum, Guid, DateTime, etc.
/// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path. /// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path.
@ -240,6 +253,7 @@ public abstract class TypeMetadataBase
var serializableAttr = type.GetCustomAttribute<AcBinarySerializableAttribute>(inherit: false); var serializableAttr = type.GetCustomAttribute<AcBinarySerializableAttribute>(inherit: false);
EnableMetadataFeature = serializableAttr == null || serializableAttr.EnableMetadataFeature; EnableMetadataFeature = serializableAttr == null || serializableAttr.EnableMetadataFeature;
EnablePropertyFilterFeature = serializableAttr == null || serializableAttr.EnablePropertyFilterFeature;
if (serializableAttr is { EnableIdTrackingFeature: false }) if (serializableAttr is { EnableIdTrackingFeature: false })
{ {
IsIId = false; IsIId = false;