Migrate UseMetadata to inline format, remove metadata footer

Refactored AcBinarySerializer to write type property metadata inline
after the ObjectWithMetadata marker, eliminating the need for a
separate metadata footer section. Updated serialization, deserialization,
and diagnostic test logic to support the new inline metadata format.
Also updated settings.local.json to allow "Bash(git stash:*)" commands.
This commit is contained in:
Loretta 2026-02-04 16:04:53 +01:00
parent 18370879ec
commit 1410ee71f0
5 changed files with 107 additions and 142 deletions

View File

@ -30,7 +30,8 @@
"Bash(dotnet new:*)", "Bash(dotnet new:*)",
"Bash(Remove-Item \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Toons\\\\AcToonSerializer.RelationshipDetection.cs\")", "Bash(Remove-Item \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Toons\\\\AcToonSerializer.RelationshipDetection.cs\")",
"Bash(find:*)", "Bash(find:*)",
"Bash(dir:*)" "Bash(dir:*)",
"Bash(git stash:*)"
] ]
} }
} }

View File

@ -262,20 +262,55 @@ public class AcBinarySerializerDiagnosticTests
// Skip any header data (strings interning, etc.) // Skip any header data (strings interning, etc.)
// New format uses PropertyIndex directly - no metadata header with property names // New format uses PropertyIndex directly - no metadata header with property names
// Find Object marker (0x19) // Find Object marker (0x19) or ObjectWithMetadata marker (0x1F)
while (pos < binary.Length && binary[pos] != 0x19) while (pos < binary.Length && binary[pos] != 0x19 && binary[pos] != 0x1F)
{ {
pos++; pos++;
} }
Console.WriteLine($"\n=== BODY (starts at position {pos}) ==="); Console.WriteLine($"\n=== BODY (starts at position {pos}) ===");
// The body should start with Object marker (0x19) // The body should start with Object (0x19) or ObjectWithMetadata (0x1F) marker
var bodyStart = pos; var bodyStart = pos;
var objectMarker = binary[pos++]; var objectMarker = binary[pos++];
Console.WriteLine($"Object marker: 0x{objectMarker:X2} (expected 0x19 for Object)"); Console.WriteLine($"Object marker: 0x{objectMarker:X2} (0x19=Object, 0x1F=ObjectWithMetadata)");
Assert.AreEqual(0x19, objectMarker, "Object marker should be 0x19"); Assert.IsTrue(objectMarker == 0x19 || objectMarker == 0x1F,
$"Object marker should be 0x19 or 0x1F, got 0x{objectMarker:X2}");
// If ObjectWithMetadata (0x1F), skip inline metadata
if (objectMarker == 0x1F)
{
// propNameHash (4 bytes)
var propNameHash = BitConverter.ToInt32(binary, pos);
pos += 4;
Console.WriteLine($"PropNameHash: 0x{propNameHash:X8}");
// First occurrence: propCount (VarUInt) + property hashes
// VarUInt: if top bit is set, continue reading
var propCountByte = binary[pos];
int inlinePropCount;
if ((propCountByte & 0x80) == 0)
{
inlinePropCount = propCountByte;
pos++;
}
else
{
// Multi-byte VarUInt - simplified 2-byte parsing
inlinePropCount = (propCountByte & 0x7F) | (binary[pos + 1] << 7);
pos += 2;
}
Console.WriteLine($"Inline metadata propCount: {inlinePropCount}");
// Skip property hashes (4 bytes each)
for (int h = 0; h < inlinePropCount; h++)
{
var hash = BitConverter.ToInt32(binary, pos);
Console.WriteLine($" Property hash [{h}]: 0x{hash:X8}");
pos += 4;
}
}
// Read ref ID (if reference handling is enabled) // Read ref ID (if reference handling is enabled)
// VarInt: if top bit is set, continue reading // VarInt: if top bit is set, continue reading
var refIdByte = binary[pos]; var refIdByte = binary[pos];
@ -292,43 +327,50 @@ public class AcBinarySerializerDiagnosticTests
pos += 2; // Skip for now pos += 2; // Skip for now
} }
Console.WriteLine($"RefId: {refId}"); Console.WriteLine($"RefId: {refId}");
// Read property count in body // Read property count in body
var bodyPropCount = binary[pos++]; var bodyPropCount = binary[pos++];
Console.WriteLine($"Property count in body: {bodyPropCount}"); Console.WriteLine($"Property count in body: {bodyPropCount}");
Console.WriteLine($"\n=== BODY PROPERTIES (PropertyIndex format) ==="); Console.WriteLine($"\n=== BODY PROPERTIES ===");
for (int i = 0; i < bodyPropCount && pos < binary.Length; i++) for (int i = 0; i < bodyPropCount && pos < binary.Length; i++)
{ {
var propIndex = binary[pos++]; // This is now PropertyIndex (alphabetical order) // Log the value (no PropertyIndex in inline metadata mode — properties are in hash order)
Console.WriteLine($" Body property [{i}]: PropertyIndex={propIndex}");
// Skip the value (simplified - just log)
var valueType = binary[pos]; var valueType = binary[pos];
if (valueType == 0x14) // DateTime if (valueType == 0x14) // DateTime
{ {
Console.WriteLine($" -> DateTime (9 bytes)"); Console.WriteLine($" Property [{i}]: DateTime (9 bytes)");
pos += 10; // type + 9 bytes pos += 10; // type + 9 bytes
} }
else if (valueType >= 0xD0 && valueType <= 0xE7) // TinyInt else if (valueType >= 0xC0 && valueType <= 0xFF) // TinyInt (192-255)
{ {
var tinyValue = valueType - 0xD0; var tinyValue = valueType - 192 - 16;
Console.WriteLine($" -> TinyInt value: {tinyValue}"); Console.WriteLine($" Property [{i}]: TinyInt value: {tinyValue}");
pos += 1; pos += 1;
} }
else if (valueType == 0x03) // False else if (valueType == 0x02) // False (BinaryTypeCode.False = 2)
{ {
Console.WriteLine($" -> Boolean: false"); Console.WriteLine($" Property [{i}]: Boolean: false");
pos += 1; pos += 1;
} }
else if (valueType == 0x02) // True else if (valueType == 0x01) // True (BinaryTypeCode.True = 1)
{ {
Console.WriteLine($" -> Boolean: true"); Console.WriteLine($" Property [{i}]: Boolean: true");
pos += 1;
}
else if (valueType == 0x00) // Null
{
Console.WriteLine($" Property [{i}]: Null");
pos += 1;
}
else if (valueType == 0xBF) // PropertySkip
{
Console.WriteLine($" Property [{i}]: PropertySkip (default/null)");
pos += 1; pos += 1;
} }
else else
{ {
Console.WriteLine($" -> Unknown type: 0x{valueType:X2}"); Console.WriteLine($" Property [{i}]: Unknown type: 0x{valueType:X2}");
break; break;
} }
} }

View File

@ -195,44 +195,13 @@ public static partial class AcBinaryDeserializer
_nextDupPosition = _dupData[0]; _nextDupPosition = _dupData[0];
} }
// Read UseMetadata footer section (per-type property hashes) // Metadata is now inline in the body (not in footer).
if (HasMetadata && _position < _buffer.Length) // No ReadMetadataFooter() call needed.
{
ReadMetadataFooter();
}
// Seek back to data position // Seek back to data position
_position = dataPosition; _position = dataPosition;
} }
/// <summary>
/// UseMetadata footer olvasása: entry-k flat array-be.
/// Formátum: [entryCount (VarUInt)]
/// entry-nként: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]...
/// Entry 0 = root, 1+ = nested object-ek.
/// </summary>
private void ReadMetadataFooter()
{
var entryCount = (int)ReadVarUInt();
for (var i = 0; i < entryCount; i++)
{
EnsureAvailable(4);
var propNameHash = Unsafe.ReadUnaligned<int>(ref Unsafe.AsRef(in _buffer[_position]));
_position += 4;
var propCount = (int)ReadVarUInt();
var hashes = new int[propCount];
for (var p = 0; p < propCount; p++)
{
EnsureAvailable(4);
hashes[p] = Unsafe.ReadUnaligned<int>(ref Unsafe.AsRef(in _buffer[_position]));
_position += 4;
}
ContextClass.RegisterFooterEntry(i, propNameHash, hashes);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte ReadByte() => ReadByteInternal(); public byte ReadByte() => ReadByteInternal();

View File

@ -74,14 +74,6 @@ public static partial class AcBinarySerializer
private const int PropertyIndexBufferMaxCache = 512; private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512; private const int PropertyStateBufferMaxCache = 512;
private const int InitialInternCapacity = 32; private const int InitialInternCapacity = 32;
/// <summary>
/// UseMetadata footer bejegyzések sorrendben.
/// Minden bejegyzés egy (Type, propertyHashes) pár.
/// A 0. elem mindig a root object.
/// A body-ban a nested object-eknél az index (VarUInt) kerül kiírásra.
/// </summary>
private List<MetadataFooterEntry>? _metadataEntries;
private byte[] _buffer; private byte[] _buffer;
private int _position; private int _position;
private int _initialBufferSize; private int _initialBufferSize;
@ -129,7 +121,11 @@ public static partial class AcBinarySerializer
/// <summary> /// <summary>
/// True if we need footer position in header (string interning OR reference handling OR metadata). /// True if we need footer position in header (string interning OR reference handling OR metadata).
/// </summary> /// </summary>
public bool HasFooter => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None || UseMetadata; /// <summary>
/// True if we need footer position in header (string interning OR reference handling).
/// UseMetadata no longer uses footer — metadata is inline in the body.
/// </summary>
public bool HasFooter => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None;
public bool UseMetadata => Options.UseMetadata; public bool UseMetadata => Options.UseMetadata;
public byte MinStringInternLength => Options.MinStringInternLength; public byte MinStringInternLength => Options.MinStringInternLength;
public byte MaxStringInternLength => Options.MaxStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength;
@ -180,7 +176,6 @@ public static partial class AcBinarySerializer
//_refTracker.Reset(); //_refTracker.Reset();
_stringInternMap?.Reset(); _stringInternMap?.Reset();
_metadataEntries?.Clear();
_nextCacheIndex = 0; _nextCacheIndex = 0;
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache) if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
@ -367,68 +362,37 @@ public static partial class AcBinarySerializer
#region UseMetadata Type Tracking #region UseMetadata Type Tracking
/// <summary> /// <summary>
/// Egy footer bejegyzés: típus propNameHash + property hash-ek. /// Regisztrálja a típust UseMetadata módban.
/// </summary> /// Visszaadja true-t ha ez az első előfordulás (inline hash-eket kell írni),
internal readonly struct MetadataFooterEntry /// false-t ha ismételt (csak propNameHash kell).
{
public readonly int PropNameHash; // FNV-1a hash a típus nevéből
public readonly int[] PropertyHashes; // property name hash-ek sorrendben
public MetadataFooterEntry(int propNameHash, int[] propertyHashes)
{
PropNameHash = propNameHash;
PropertyHashes = propertyHashes;
}
}
/// <summary>
/// Regisztrálja a típust a UseMetadata footer-be.
/// Visszaadja a footer index-et. Ha már regisztrálva van (wrapper.MetadataFooterIndex >= 0),
/// a meglévő index-et adja vissza. Nincs Dictionary lookup — a wrapper tárolja az indexet.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RegisterMetadataType(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper) public bool RegisterMetadataType(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
{ {
if (wrapper.MetadataFooterIndex >= 0) if (wrapper.MetadataFooterIndex >= 0)
return wrapper.MetadataFooterIndex; return false; // ismételt
return RegisterMetadataTypeSlow(wrapper); wrapper.MetadataFooterIndex = 0; // jelöljük hogy már regisztrálva
} return true; // első előfordulás
[MethodImpl(MethodImplOptions.NoInlining)]
private int RegisterMetadataTypeSlow(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
{
_metadataEntries ??= new List<MetadataFooterEntry>();
// PropNameHash and MetadataPropertyHashes are lazy-computed once per type in metadata.
// Duplicate hash validation also happens once (in MetadataPropertyHashes getter).
var index = _metadataEntries.Count;
_metadataEntries.Add(new MetadataFooterEntry(
wrapper.Metadata.PropNameHash,
wrapper.Metadata.MetadataPropertyHashes));
wrapper.MetadataFooterIndex = index;
return index;
} }
/// <summary> /// <summary>
/// UseMetadata footer kiírása. /// Inline metadata kiírása az ObjectWithMetadata marker után.
/// Formátum: [entryCount (VarUInt)] /// Első előfordulás: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]...
/// entry-nként: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]... /// Ismételt: [propNameHash (4b)]
/// </summary> /// </summary>
public void WriteMetadataFooter() [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInlineMetadata(BinarySerializeTypeMetadata metadata, bool isFirstOccurrence)
{ {
if (_metadataEntries == null || _metadataEntries.Count == 0) return; WriteRaw(metadata.PropNameHash);
WriteVarUInt((uint)_metadataEntries.Count); if (isFirstOccurrence)
for (var i = 0; i < _metadataEntries.Count; i++)
{ {
var entry = _metadataEntries[i]; var hashes = metadata.MetadataPropertyHashes;
WriteRaw(entry.PropNameHash); WriteVarUInt((uint)hashes.Length);
WriteVarUInt((uint)entry.PropertyHashes.Length); for (var i = 0; i < hashes.Length; i++)
for (var j = 0; j < entry.PropertyHashes.Length; j++)
{ {
WriteRaw(entry.PropertyHashes[j]); WriteRaw(hashes[i]);
} }
} }
} }
@ -1063,27 +1027,22 @@ public static partial class AcBinarySerializer
{ {
var dupCount = GetDupCount(); // Shared counter: string intern + ID tracking var dupCount = GetDupCount(); // Shared counter: string intern + ID tracking
var hasInternTable = dupCount > 0; var hasInternTable = dupCount > 0;
var hasMetadataFooter = UseMetadata && _metadataEntries is { Count: > 0 };
// Footer: write merged intern entries (string + ID) // Footer: write merged intern entries (string + ID)
// Metadata footer is no longer written here — metadata is inline in the body.
var footerPosition = 0; var footerPosition = 0;
if (hasInternTable || hasMetadataFooter) if (hasInternTable)
{ {
footerPosition = _position; footerPosition = _position;
// Intern footer // Intern footer
WriteVarUInt((uint)dupCount); WriteVarUInt((uint)dupCount);
if (hasInternTable) WriteInternedFooter();
WriteInternedFooter();
// Metadata footer (per-type property hashes)
if (hasMetadataFooter)
WriteMetadataFooter();
} }
// Write header // Write header
var flags = BinaryTypeCode.HeaderFlagsBase; var flags = BinaryTypeCode.HeaderFlagsBase;
if (hasMetadataFooter) if (UseMetadata)
flags |= BinaryTypeCode.HeaderFlag_Metadata; flags |= BinaryTypeCode.HeaderFlag_Metadata;
// Encode ReferenceHandlingMode using separate bits // Encode ReferenceHandlingMode using separate bits
if (ReferenceHandling == ReferenceHandlingMode.OnlyId) if (ReferenceHandling == ReferenceHandlingMode.OnlyId)

View File

@ -775,22 +775,16 @@ public static partial class AcBinarySerializer
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
// Wire format: // Wire format:
// - IId types: [Object/ObjectWithMetadata][props 0-tól...] - Id a props-ban, nincs extra // - UseMetadata=false: [Object][props...]
// - Non-IId + All: [Object/ObjectWithMetadata][props 0-tól...] - no hashcode prefix // - UseMetadata=true, első: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...]
// - Ref=Off: [Object/ObjectWithMetadata][props 0-tól...] - no prefix // - UseMetadata=true, ismételt: [ObjectWithMetadata][propNameHash (4b)][props...]
// ObjectRef format: // ObjectRef: [ObjectRef][cacheIndex]
// - IId: [ObjectRef][cacheIndex]
// - Non-IId: [ObjectRef][cacheIndex]
//
// UseMetadata:
// - Root (depth==0): [Object] marker, footer entry 0
// - Nested: [ObjectWithMetadata][footerIndex (VarUInt)]
// UseMetadata: regisztráljuk a típust a footer-be (index kell a marker kiíráshoz) // UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
var metadataFooterIndex = -1; var isFirstMetadataOccurrence = false;
if (context.UseMetadata) if (context.UseMetadata)
{ {
metadataFooterIndex = context.RegisterMetadataType(wrapper); isFirstMetadataOccurrence = context.RegisterMetadataType(wrapper);
} }
if (context.UseTypeReferenceHandling(metadata)) if (context.UseTypeReferenceHandling(metadata))
@ -840,11 +834,11 @@ public static partial class AcBinarySerializer
} }
} }
// Marker kiírása: UseMetadata nested → ObjectWithMetadata + footer index, egyébként Object // Marker kiírása: UseMetadata → ObjectWithMetadata + inline metadata, egyébként Object
if (context.UseMetadata && isNested) if (context.UseMetadata)
{ {
context.WriteByte(BinaryTypeCode.ObjectWithMetadata); context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
context.WriteVarUInt((uint)metadataFooterIndex); context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
} }
else else
{ {