Refactor: hoist interned string decode to context methods
- Moved interned string decode logic to BinaryDeserializationContext instance methods, reducing duplication and unifying SGen, runtime, and cross-type paths. - Updated SGen-emitted code and TypeReaderTable to use new context methods. - Added performance TODO (ACCORE-BIN-T-K9M3) documenting rationale and acceptance. - Clarified AcBinarySerializableAttribute XML docs. - Added repo-scoped nuget.config for deterministic restore. - Updated settings.local.json with new Bash/dev commands. - Minor code and comment cleanups for clarity.
This commit is contained in:
parent
67b04612a4
commit
8293a6edd1
|
|
@ -83,7 +83,9 @@
|
||||||
"Bash(dotnet test *)",
|
"Bash(dotnet test *)",
|
||||||
"Read(//tmp/**)",
|
"Read(//tmp/**)",
|
||||||
"Bash(git -C H:/Applications/Aycode/Source/AyCode.Core log -p --all -S \"MaxDepth\" -- AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs)",
|
"Bash(git -C H:/Applications/Aycode/Source/AyCode.Core log -p --all -S \"MaxDepth\" -- AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs)",
|
||||||
"Bash(git -C H:/Applications/Aycode/Source/AyCode.Core show ac6e66f^:AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs)"
|
"Bash(git -C H:/Applications/Aycode/Source/AyCode.Core show ac6e66f^:AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs)",
|
||||||
|
"Bash(ls -la \"H:\\\\Applications\\\\Aycode\\\\Source\\\\\" 2>&1 && echo \"---\" && ls -la \"H:\\\\Applications\\\\Aycode\\\\\" 2>&1)",
|
||||||
|
"Bash(dotnet publish *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2008,32 +2008,14 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
sb.AppendLine($"{i} {a} = salen == 0 ? string.Empty : context.ReadAsciiBytesAsString(salen);");
|
sb.AppendLine($"{i} {a} = salen == 0 ? string.Empty : context.ReadAsciiBytesAsString(salen);");
|
||||||
sb.AppendLine($"{i} break;");
|
sb.AppendLine($"{i} break;");
|
||||||
sb.AppendLine($"{i} }}");
|
sb.AppendLine($"{i} }}");
|
||||||
// H2Q6 interning — Small tier
|
// H2Q6 interning — Small / Medium tiers. Wire-decode body is shared with the runtime path
|
||||||
|
// (TypeReaderTable + cross-type populate) — see context.ReadAndRegisterInternedStringSmall/Medium.
|
||||||
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstSmall:");
|
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstSmall:");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {a} = context.ReadAndRegisterInternedStringSmall();");
|
||||||
sb.AppendLine($"{i} context.DisableStringCaching();");
|
|
||||||
sb.AppendLine($"{i} var iscIdx = (int)context.ReadVarUInt();");
|
|
||||||
sb.AppendLine($"{i} var ishdr = context.ReadTwoBytesUnsafe();");
|
|
||||||
sb.AppendLine($"{i} var ischarLen = (byte)ishdr;");
|
|
||||||
sb.AppendLine($"{i} var isbyteLen = (byte)(ishdr >> 8);");
|
|
||||||
sb.AppendLine($"{i} var isv = isbyteLen == 0 ? string.Empty : context.ReadStringUtf8WithCharLen(ischarLen, isbyteLen);");
|
|
||||||
sb.AppendLine($"{i} context.RegisterInternedValueAt(iscIdx, isv);");
|
|
||||||
sb.AppendLine($"{i} {a} = isv;");
|
|
||||||
sb.AppendLine($"{i} break;");
|
sb.AppendLine($"{i} break;");
|
||||||
sb.AppendLine($"{i} }}");
|
|
||||||
// H2Q6 interning — Medium tier — single uint header read
|
|
||||||
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstMedium:");
|
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstMedium:");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {a} = context.ReadAndRegisterInternedStringMedium();");
|
||||||
sb.AppendLine($"{i} context.DisableStringCaching();");
|
|
||||||
sb.AppendLine($"{i} var imcIdx = (int)context.ReadVarUInt();");
|
|
||||||
sb.AppendLine($"{i} var impacked = context.ReadUInt32Unsafe();");
|
|
||||||
sb.AppendLine($"{i} var imcharLen = (ushort)impacked;");
|
|
||||||
sb.AppendLine($"{i} var imbyteLen = (ushort)(impacked >> 16);");
|
|
||||||
sb.AppendLine($"{i} var imv = imbyteLen == 0 ? string.Empty : context.ReadStringUtf8WithCharLen(imcharLen, imbyteLen);");
|
|
||||||
sb.AppendLine($"{i} context.RegisterInternedValueAt(imcIdx, imv);");
|
|
||||||
sb.AppendLine($"{i} {a} = imv;");
|
|
||||||
sb.AppendLine($"{i} break;");
|
sb.AppendLine($"{i} break;");
|
||||||
sb.AppendLine($"{i} }}");
|
|
||||||
sb.AppendLine($"{i} case BinaryTypeCode.Null:");
|
sb.AppendLine($"{i} case BinaryTypeCode.Null:");
|
||||||
sb.AppendLine($"{i} {a} = null;");
|
sb.AppendLine($"{i} {a} = null;");
|
||||||
sb.AppendLine($"{i} break;");
|
sb.AppendLine($"{i} break;");
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,7 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
||||||
/// The wrapper contains metadata (from GlobalMetadataCache) + per-context tracking state.
|
/// The wrapper contains metadata (from GlobalMetadataCache) + per-context tracking state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public TypeMetadataWrapper<TMetadata> GetWrapper(
|
public TypeMetadataWrapper<TMetadata> GetWrapper([DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type type)
|
||||||
[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type type)
|
|
||||||
{
|
{
|
||||||
if (_wrappers.TryGetValue(type, out var wrapper))
|
if (_wrappers.TryGetValue(type, out var wrapper))
|
||||||
return wrapper;
|
return wrapper;
|
||||||
|
|
@ -108,8 +107,7 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private TypeMetadataWrapper<TMetadata> GetWrapperSlow(
|
private TypeMetadataWrapper<TMetadata> GetWrapperSlow([DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type type)
|
||||||
[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type type)
|
|
||||||
{
|
{
|
||||||
// Get metadata from global cache (thread-safe).
|
// Get metadata from global cache (thread-safe).
|
||||||
// ConcurrentDictionary.GetOrAdd's Func<TKey, TValue> overload drops DAMs at the delegate
|
// ConcurrentDictionary.GetOrAdd's Func<TKey, TValue> overload drops DAMs at the delegate
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,8 @@ namespace AyCode.Core.Serializers.Attributes;
|
||||||
/// </list>
|
/// </list>
|
||||||
///
|
///
|
||||||
/// <para>Set a flag to <c>false</c> only when you can guarantee no consumer will ever need that
|
/// <para>Set a flag to <c>false</c> only when you can guarantee no consumer will ever need that
|
||||||
/// feature on this type (typical for high-throughput message DTOs where wire size and serialize
|
/// feature on this type AND the marginal hot-path speedup matters for the workload. Otherwise
|
||||||
/// CPU dominate, and a feature like <c>PropertyFilter</c> is genuinely never used). Otherwise leave
|
/// leave it at the default <c>true</c>.</para>
|
||||||
/// it at the default <c>true</c>.</para>
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
|
||||||
public sealed class AcBinarySerializableAttribute : Attribute
|
public sealed class AcBinarySerializableAttribute : Attribute
|
||||||
|
|
|
||||||
|
|
@ -619,6 +619,57 @@ public static partial class AcBinaryDeserializer
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// H2Q6 StringInternFirstSmall reader: wire <c>[cacheIdx:VarUInt][charLen:8][utf8Len:8][bytes]</c>
|
||||||
|
/// after the marker has been consumed. Registers the decoded string in the intern cache and returns it.
|
||||||
|
/// Single source of wire-decode for this marker — shared by the runtime <c>TypeReaderTable</c>
|
||||||
|
/// dispatch, the cross-type populate path, and the SGen-emitted string-property switch.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
internal string ReadAndRegisterInternedStringSmall()
|
||||||
|
{
|
||||||
|
// First interning marker proves payload uses string interning → plain String entries
|
||||||
|
// appear only once, so _stringCache would never hit on them.
|
||||||
|
DisableStringCaching();
|
||||||
|
var cacheIndex = (int)ReadVarUInt();
|
||||||
|
var header = ReadTwoBytesUnsafe();
|
||||||
|
var charLength = (byte)header;
|
||||||
|
var byteLength = (byte)(header >> 8);
|
||||||
|
if (byteLength == 0)
|
||||||
|
{
|
||||||
|
RegisterInternedValueAt(cacheIndex, string.Empty);
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
var str = ReadStringUtf8WithCharLen(charLength, byteLength);
|
||||||
|
RegisterInternedValueAt(cacheIndex, str);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// H2Q6 StringInternFirstMedium reader: wire <c>[cacheIdx:VarUInt][charLen:16 LE][utf8Len:16 LE][bytes]</c>.
|
||||||
|
/// Registers the decoded string in the intern cache and returns it. (Big tier never engages on the
|
||||||
|
/// interning path — see <see cref="BinaryTypeCode"/> H2Q6 layout comment.) Shared by runtime
|
||||||
|
/// dispatch + SGen-emit (same rationale as <see cref="ReadAndRegisterInternedStringSmall"/>).
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
internal string ReadAndRegisterInternedStringMedium()
|
||||||
|
{
|
||||||
|
DisableStringCaching();
|
||||||
|
var cacheIndex = (int)ReadVarUInt();
|
||||||
|
// Pack charLen:16 | utf8Len:16 read in a single uint load
|
||||||
|
var packed = ReadUInt32Unsafe();
|
||||||
|
var charLength = (ushort)packed;
|
||||||
|
var byteLength = (ushort)(packed >> 16);
|
||||||
|
if (byteLength == 0)
|
||||||
|
{
|
||||||
|
RegisterInternedValueAt(cacheIndex, string.Empty);
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
var str = ReadStringUtf8WithCharLen(charLength, byteLength);
|
||||||
|
RegisterInternedValueAt(cacheIndex, str);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Full-content hash for string caching.
|
/// Full-content hash for string caching.
|
||||||
/// CRITICAL: DO NOT SIMPLIFY <20> prevents hash collisions for similar property names.
|
/// CRITICAL: DO NOT SIMPLIFY <20> prevents hash collisions for similar property names.
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,8 @@ public static partial class AcBinaryDeserializer
|
||||||
readers[BinaryTypeCode.StringInterned] = static (ctx, _) => ctx.GetInternedString((int)ctx.ReadVarUInt());
|
readers[BinaryTypeCode.StringInterned] = static (ctx, _) => ctx.GetInternedString((int)ctx.ReadVarUInt());
|
||||||
readers[BinaryTypeCode.StringEmpty] = static (_, _) => string.Empty;
|
readers[BinaryTypeCode.StringEmpty] = static (_, _) => string.Empty;
|
||||||
// H2Q6 interning tier readers (Compact mode only — Big tier never engages on interning path)
|
// H2Q6 interning tier readers (Compact mode only — Big tier never engages on interning path)
|
||||||
readers[BinaryTypeCode.StringInternFirstSmall] = static (ctx, _) => ReadAndRegisterInternedStringSmall(ctx);
|
readers[BinaryTypeCode.StringInternFirstSmall] = static (ctx, _) => ctx.ReadAndRegisterInternedStringSmall();
|
||||||
readers[BinaryTypeCode.StringInternFirstMedium] = static (ctx, _) => ReadAndRegisterInternedStringMedium(ctx);
|
readers[BinaryTypeCode.StringInternFirstMedium] = static (ctx, _) => ctx.ReadAndRegisterInternedStringMedium();
|
||||||
readers[BinaryTypeCode.StringAscii] = static (ctx, _) => ReadPlainStringAscii(ctx);
|
readers[BinaryTypeCode.StringAscii] = static (ctx, _) => ReadPlainStringAscii(ctx);
|
||||||
readers[BinaryTypeCode.DateTime] = static (ctx, _) => ctx.ReadDateTimeUnsafe();
|
readers[BinaryTypeCode.DateTime] = static (ctx, _) => ctx.ReadDateTimeUnsafe();
|
||||||
readers[BinaryTypeCode.DateTimeOffset] = static (ctx, _) => ctx.ReadDateTimeOffsetUnsafe();
|
readers[BinaryTypeCode.DateTimeOffset] = static (ctx, _) => ctx.ReadDateTimeOffsetUnsafe();
|
||||||
|
|
@ -1067,10 +1067,10 @@ public static partial class AcBinaryDeserializer
|
||||||
propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt()));
|
propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt()));
|
||||||
return true;
|
return true;
|
||||||
case BinaryTypeCode.StringInternFirstSmall:
|
case BinaryTypeCode.StringInternFirstSmall:
|
||||||
propInfo.SetValue(target, ReadAndRegisterInternedStringSmall(context));
|
propInfo.SetValue(target, context.ReadAndRegisterInternedStringSmall());
|
||||||
return true;
|
return true;
|
||||||
case BinaryTypeCode.StringInternFirstMedium:
|
case BinaryTypeCode.StringInternFirstMedium:
|
||||||
propInfo.SetValue(target, ReadAndRegisterInternedStringMedium(context));
|
propInfo.SetValue(target, context.ReadAndRegisterInternedStringMedium());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -1239,55 +1239,10 @@ public static partial class AcBinaryDeserializer
|
||||||
return context.ReadAsciiBytesAsString(length);
|
return context.ReadAsciiBytesAsString(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// ReadAndRegisterInternedStringSmall / Medium moved to BinaryDeserializationContext as instance
|
||||||
/// H2Q6 StringInternFirstSmall reader: wire <c>[cacheIdx:VarUInt][charLen:8][utf8Len:8][bytes]</c>
|
// methods — single source of wire-decode shared by TypeReaderTable dispatch, PopulateProperty
|
||||||
/// after the marker has been consumed. Registers the decoded string in the intern cache.
|
// cross-type path, and the SGen-emitted string-property switch. See
|
||||||
/// </summary>
|
// `BinaryDeserializationContext.Read.cs` for the implementations.
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static string ReadAndRegisterInternedStringSmall<TInput>(BinaryDeserializationContext<TInput> context)
|
|
||||||
where TInput : struct, IBinaryInputBase
|
|
||||||
{
|
|
||||||
// First interning marker proves payload uses string interning → plain String entries
|
|
||||||
// appear only once, so _stringCache would never hit on them.
|
|
||||||
context.DisableStringCaching();
|
|
||||||
var cacheIndex = (int)context.ReadVarUInt();
|
|
||||||
var header = context.ReadTwoBytesUnsafe();
|
|
||||||
var charLength = (byte)header;
|
|
||||||
var byteLength = (byte)(header >> 8);
|
|
||||||
if (byteLength == 0)
|
|
||||||
{
|
|
||||||
context.RegisterInternedValueAt(cacheIndex, string.Empty);
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
var str = context.ReadStringUtf8WithCharLen(charLength, byteLength);
|
|
||||||
context.RegisterInternedValueAt(cacheIndex, str);
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// H2Q6 StringInternFirstMedium reader: wire <c>[cacheIdx:VarUInt][charLen:16 LE][utf8Len:16 LE][bytes]</c>.
|
|
||||||
/// Registers the decoded string in the intern cache. (Big tier never engages on the interning path —
|
|
||||||
/// see <see cref="BinaryTypeCode"/> H2Q6 layout comment.)
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static string ReadAndRegisterInternedStringMedium<TInput>(BinaryDeserializationContext<TInput> context)
|
|
||||||
where TInput : struct, IBinaryInputBase
|
|
||||||
{
|
|
||||||
context.DisableStringCaching();
|
|
||||||
var cacheIndex = (int)context.ReadVarUInt();
|
|
||||||
// Pack charLen:16 | utf8Len:16 read in a single uint load
|
|
||||||
var packed = context.ReadUInt32Unsafe();
|
|
||||||
var charLength = (ushort)packed;
|
|
||||||
var byteLength = (ushort)(packed >> 16);
|
|
||||||
if (byteLength == 0)
|
|
||||||
{
|
|
||||||
context.RegisterInternedValueAt(cacheIndex, string.Empty);
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
var str = context.ReadStringUtf8WithCharLen(charLength, byteLength);
|
|
||||||
context.RegisterInternedValueAt(cacheIndex, str);
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
///// <summary>
|
///// <summary>
|
||||||
///// Read a string and register it in the intern table for future references.
|
///// Read a string and register it in the intern table for future references.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Repo-scoped NuGet source declaration. Encodes the workspace-dependency contract from
|
||||||
|
`.github/copilot-instructions.md` (`@repo.own-dep-repos = []`) and `CONSUMERS.md`
|
||||||
|
(AyCode.Core is Layer 0 — no upstream repos):
|
||||||
|
|
||||||
|
AyCode.Core has NO external repo dependencies. Therefore restore MUST NOT depend
|
||||||
|
on any machine-level or user-level NuGet feed pointing at a sibling repo's build
|
||||||
|
output (e.g. Mango.Nop.Core/bin/...). Such feeds, when configured globally on a
|
||||||
|
developer machine for another repo's workflow, would otherwise be probed during
|
||||||
|
`dotnet restore` for THIS repo too — and produce NU1301 errors when their paths
|
||||||
|
don't exist, even though no package from them is actually required.
|
||||||
|
|
||||||
|
The `<clear />` element explicitly drops all parent NuGet configurations
|
||||||
|
(machine-level + user-level + any walk-up Directory-level config) for this repo's
|
||||||
|
scope. Only `nuget.org` remains, matching the public packages declared in the
|
||||||
|
csproj files (AutoMapper, MessagePack, MemoryPack, Newtonsoft.Json, Microsoft.*).
|
||||||
|
|
||||||
|
Determinism: every dev machine + CI agent resolves the same source set. Side
|
||||||
|
effect-free with respect to other repos sharing the same developer machine — their
|
||||||
|
own per-repo nuget.config (or lack thereof) is unaffected.
|
||||||
|
-->
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
Loading…
Reference in New Issue