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 *)",
|
||||
"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 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} break;");
|
||||
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} {{");
|
||||
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} {a} = context.ReadAndRegisterInternedStringSmall();");
|
||||
sb.AppendLine($"{i} break;");
|
||||
sb.AppendLine($"{i} }}");
|
||||
// H2Q6 interning — Medium tier — single uint header read
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstMedium:");
|
||||
sb.AppendLine($"{i} {{");
|
||||
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} {a} = context.ReadAndRegisterInternedStringMedium();");
|
||||
sb.AppendLine($"{i} break;");
|
||||
sb.AppendLine($"{i} }}");
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.Null:");
|
||||
sb.AppendLine($"{i} {a} = null;");
|
||||
sb.AppendLine($"{i} break;");
|
||||
|
|
|
|||
|
|
@ -98,8 +98,7 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
|||
/// The wrapper contains metadata (from GlobalMetadataCache) + per-context tracking state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TypeMetadataWrapper<TMetadata> GetWrapper(
|
||||
[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type type)
|
||||
public TypeMetadataWrapper<TMetadata> GetWrapper([DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type type)
|
||||
{
|
||||
if (_wrappers.TryGetValue(type, out var wrapper))
|
||||
return wrapper;
|
||||
|
|
@ -108,8 +107,7 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private TypeMetadataWrapper<TMetadata> GetWrapperSlow(
|
||||
[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type type)
|
||||
private TypeMetadataWrapper<TMetadata> GetWrapperSlow([DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type type)
|
||||
{
|
||||
// Get metadata from global cache (thread-safe).
|
||||
// ConcurrentDictionary.GetOrAdd's Func<TKey, TValue> overload drops DAMs at the delegate
|
||||
|
|
|
|||
|
|
@ -27,9 +27,8 @@ namespace AyCode.Core.Serializers.Attributes;
|
|||
/// </list>
|
||||
///
|
||||
/// <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
|
||||
/// CPU dominate, and a feature like <c>PropertyFilter</c> is genuinely never used). Otherwise leave
|
||||
/// it at the default <c>true</c>.</para>
|
||||
/// feature on this type AND the marginal hot-path speedup matters for the workload. Otherwise
|
||||
/// leave it at the default <c>true</c>.</para>
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AcBinarySerializableAttribute : Attribute
|
||||
|
|
|
|||
|
|
@ -619,6 +619,57 @@ public static partial class AcBinaryDeserializer
|
|||
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>
|
||||
/// Full-content hash for string caching.
|
||||
/// 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.StringEmpty] = static (_, _) => string.Empty;
|
||||
// H2Q6 interning tier readers (Compact mode only — Big tier never engages on interning path)
|
||||
readers[BinaryTypeCode.StringInternFirstSmall] = static (ctx, _) => ReadAndRegisterInternedStringSmall(ctx);
|
||||
readers[BinaryTypeCode.StringInternFirstMedium] = static (ctx, _) => ReadAndRegisterInternedStringMedium(ctx);
|
||||
readers[BinaryTypeCode.StringInternFirstSmall] = static (ctx, _) => ctx.ReadAndRegisterInternedStringSmall();
|
||||
readers[BinaryTypeCode.StringInternFirstMedium] = static (ctx, _) => ctx.ReadAndRegisterInternedStringMedium();
|
||||
readers[BinaryTypeCode.StringAscii] = static (ctx, _) => ReadPlainStringAscii(ctx);
|
||||
readers[BinaryTypeCode.DateTime] = static (ctx, _) => ctx.ReadDateTimeUnsafe();
|
||||
readers[BinaryTypeCode.DateTimeOffset] = static (ctx, _) => ctx.ReadDateTimeOffsetUnsafe();
|
||||
|
|
@ -1067,10 +1067,10 @@ public static partial class AcBinaryDeserializer
|
|||
propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt()));
|
||||
return true;
|
||||
case BinaryTypeCode.StringInternFirstSmall:
|
||||
propInfo.SetValue(target, ReadAndRegisterInternedStringSmall(context));
|
||||
propInfo.SetValue(target, context.ReadAndRegisterInternedStringSmall());
|
||||
return true;
|
||||
case BinaryTypeCode.StringInternFirstMedium:
|
||||
propInfo.SetValue(target, ReadAndRegisterInternedStringMedium(context));
|
||||
propInfo.SetValue(target, context.ReadAndRegisterInternedStringMedium());
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1239,55 +1239,10 @@ public static partial class AcBinaryDeserializer
|
|||
return context.ReadAsciiBytesAsString(length);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
// ReadAndRegisterInternedStringSmall / Medium moved to BinaryDeserializationContext as instance
|
||||
// methods — single source of wire-decode shared by TypeReaderTable dispatch, PopulateProperty
|
||||
// cross-type path, and the SGen-emitted string-property switch. See
|
||||
// `BinaryDeserializationContext.Read.cs` for the implementations.
|
||||
|
||||
///// <summary>
|
||||
///// 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