Track buffer growth stats in DEBUG; disable IId merge helper

Add DEBUG-only tracking of buffer growth in AcBinarySerializer for benchmarking, with stats output in console app. Expose stats via static properties and reset at serialization start. Add InternalsVisibleTo for console access. Comment out IIdCollectionMergeHelper.cs. Minor code cleanups included.
This commit is contained in:
Loretta 2026-01-29 09:41:53 +01:00
parent f778d4faa9
commit 2eca18ca3f
5 changed files with 167 additions and 110 deletions

View File

@ -43,13 +43,18 @@ public static class Program
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
#if DEBUG
private static int WarmupIterations = 5;
private static int TestIterations = 10;
#else
private static int WarmupIterations = 2000;
private static int TestIterations = 1000;
#endif
public static void Main(string[] args)
{
// Set console encoding to UTF-8 for proper Unicode character display
System.Console.OutputEncoding = System.Text.Encoding.UTF8;
System.Console.OutputEncoding = Encoding.UTF8;
var mode = args.Length > 0 ? args[0].ToLower() : "all";
@ -779,8 +784,8 @@ public static class Program
}
System.Console.WriteLine($"└{"".PadRight(6, '─')}─{"".PadRight(27, '─')}┴{"".PadRight(12, '─')}┴{"".PadRight(14, '─')}┴{"".PadRight(14, '─')}┴{"".PadRight(13, '─')}┘");
System.Console.WriteLine($"GrowBufferCount: {AyCode.Core.Serializers.Binaries.AcBinarySerializer.BinarySerializationContext.GrowBufferCount}");
System.Console.WriteLine($"GrowBufferTotalBytes: {AyCode.Core.Serializers.Binaries.AcBinarySerializer.BinarySerializationContext.GrowBufferTotalBytes:N0} bytes");
System.Console.WriteLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
System.Console.WriteLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
}
// Summary: Best serializer for each category
@ -979,8 +984,12 @@ public static class Program
sb.AppendLine($" {SerializerAcBinaryDefault} vs {SerializerMessagePack}: Size {sizePct:+0;-0}% │ Ser {serPct:+0;-0}% │ Des {desPct:+0;-0}% │ RT {rtPct:+0;-0}%");
}
sb.AppendLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
sb.AppendLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
}
// Summary comparison
sb.AppendLine();
sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ===");

View File

@ -25,4 +25,8 @@
<Folder Include="Expressions\" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="AyCode.Core.Serializers.Console" />
</ItemGroup>
</Project>

View File

@ -45,6 +45,20 @@ public static partial class AcBinarySerializer
}
}
public static int GrowBufferCount =>
#if DEBUG
BinarySerializationContext.GrowBufferCount;
#else
-1;
#endif
public static long GrowBufferTotalBytes =>
#if DEBUG
BinarySerializationContext.GrowBufferTotalBytes;
#else
-1;
#endif
/// <summary>
/// Binary serialization context. Public for generated serializers.
/// </summary>
@ -60,6 +74,20 @@ public static partial class AcBinarySerializer
private int _position;
private int _initialBufferSize;
#if DEBUG
/// <summary>
/// Counts how many times GrowBuffer was called during serialization.
/// Used for benchmarking buffer allocation efficiency.
/// </summary>
public static int GrowBufferCount { get; set; }
/// <summary>
/// Total bytes allocated by GrowBuffer during serialization.
/// Used for benchmarking buffer allocation efficiency.
/// </summary>
public static long GrowBufferTotalBytes { get; set; }
#endif
// Use shared reference tracker from AcSerializerCommon
//private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
@ -102,7 +130,7 @@ public static partial class AcBinarySerializer
public byte MinStringInternLength => Options.MinStringInternLength;
public byte MaxStringInternLength => Options.MaxStringInternLength;
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;
/// <summary>
/// Cached check for PropertyFilter != null. Set in Reset() to avoid property getter in hot loop.
/// </summary>
@ -120,7 +148,7 @@ public static partial class AcBinarySerializer
/// <summary>
/// Factory for creating BinarySerializeTypeMetadata instances.
/// </summary>
protected override Func<Type, BinarySerializeTypeMetadata> MetadataFactory
protected override Func<Type, BinarySerializeTypeMetadata> MetadataFactory
=> static t => new BinarySerializeTypeMetadata(t, HasJsonIgnoreAttribute);
public override void Reset(AcBinarySerializerOptions options)
@ -132,6 +160,9 @@ public static partial class AcBinarySerializer
_initialBufferSize = Math.Max(Options.InitialBufferCapacity, MinBufferSize);
HasPropertyFilter = Options.PropertyFilter != null;
// NOTE: GrowBufferCount és GrowBufferTotalBytes NEM nullázódik itt!
// Kumulatívan gyűjtjük a benchmark során.
if (_buffer.Length < _initialBufferSize)
{
ArrayPool<byte>.Shared.Return(_buffer);
@ -161,6 +192,10 @@ public static partial class AcBinarySerializer
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
// NOTE: GrowBufferCount <20>s GrowBufferTotalBytes nem null<6C>z<EFBFBD>dik itt,
// hogy a m<>r<EFBFBD>sek v<>g<EFBFBD>n ki tudj<64>k <20>rni az <20>rt<72>keket.
// Csak a Reset() met<65>dusban null<6C>z<EFBFBD>dnak minden <20>j fut<75>s elej<65>n.
}
@ -183,7 +218,7 @@ public static partial class AcBinarySerializer
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
}
#region String Interning
@ -409,6 +444,11 @@ public static partial class AcBinarySerializer
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = newBuffer;
#if DEBUG
GrowBufferCount++;
GrowBufferTotalBytes += newSize;
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -516,7 +556,7 @@ public static partial class AcBinarySerializer
_buffer[position] = (byte)value;
return;
}
// Multi-byte case - need to shift buffer if new encoding is longer
// For simplicity, we'll rewrite from the position
// This is rare for property counts
@ -535,10 +575,10 @@ public static partial class AcBinarySerializer
pos++;
}
currentSize++; // Include final byte without continuation bit
// Calculate new size needed
var newSize = GetVarUIntSize(value);
if (newSize == currentSize)
{
// Same size - just overwrite
@ -556,7 +596,7 @@ public static partial class AcBinarySerializer
var delta = currentSize - newSize;
Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
_position -= delta;
var tempPos = position;
while (value >= 0x80)
{
@ -572,7 +612,7 @@ public static partial class AcBinarySerializer
EnsureCapacity(delta);
Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
_position += delta;
var tempPos = position;
while (value >= 0x80)
{
@ -687,7 +727,7 @@ public static partial class AcBinarySerializer
_position += value.Length;
return;
}
// Standard path for multi-byte UTF8
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
@ -821,7 +861,7 @@ public static partial class AcBinarySerializer
{
EnsureCapacity(source.Length);
var destination = _buffer.AsSpan(_position, source.Length);
if (Vector.IsHardwareAccelerated && source.Length >= Vector<byte>.Count * 2)
{
CopyWithSimd(source, destination);
@ -830,7 +870,7 @@ public static partial class AcBinarySerializer
{
source.CopyTo(destination);
}
_position += source.Length;
}
@ -843,7 +883,7 @@ public static partial class AcBinarySerializer
var vectorSize = Vector<byte>.Count;
var i = 0;
var length = source.Length;
// Process full vectors
var vectorCount = length / vectorSize;
for (var v = 0; v < vectorCount; v++)
@ -852,7 +892,7 @@ public static partial class AcBinarySerializer
vec.CopyTo(destination.Slice(i, vectorSize));
i += vectorSize;
}
// Copy remaining bytes
if (i < length)
{
@ -890,7 +930,7 @@ public static partial class AcBinarySerializer
// Guid is 16 bytes, perfect for SIMD
var byteLength = values.Length * 16;
EnsureCapacity(byteLength);
for (var i = 0; i < values.Length; i++)
{
values[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
@ -903,7 +943,7 @@ public static partial class AcBinarySerializer
#region Header and Metadata
private int _headerPosition;
// Footer-based string interning: no estimation or shifting needed
// Header: [version][flags][footerPosition (4 bytes, only if string interning)]
// Body: data with StringInterned indices
@ -916,7 +956,7 @@ public static partial class AcBinarySerializer
public int EstimateHeaderPayloadSize()
{
var size = 0;
// Only property names are in header now
if (UseMetadata && _propertyNameList is { Count: > 0 })
{
@ -928,7 +968,7 @@ public static partial class AcBinarySerializer
size += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
return size;
}
@ -1075,7 +1115,7 @@ public static partial class AcBinarySerializer
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public bool TrackForScanning(object obj) => _refTracker.TrackForScanning(obj);
/// <summary>
/// IId-aware tracking for the scan phase.
/// First checks IId match (different instance, same Id), then falls back to ReferenceEquals.
@ -1162,12 +1202,12 @@ public static partial class AcBinarySerializer
{
var length = value.Length;
EnsureCapacity(1 + length);
// Ascii.FromUtf16: SIMD-optimized ASCII conversion
// Returns actual bytes written - if less than input length, there was a non-ASCII char
var destSpan = _buffer.AsSpan(_position + 1, length);
var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);
if (status == System.Buffers.OperationStatus.Done && bytesWritten == length)
{
// Success - write FixStr header

View File

@ -344,6 +344,10 @@ public static partial class AcBinarySerializer
private static BinarySerializationContext SerializeCore(object value, Type runtimeType, AcBinarySerializerOptions options)
{
#if DEBUG
BinarySerializationContext.GrowBufferCount = 0;
BinarySerializationContext.GrowBufferTotalBytes = 0;
#endif
var context = BinarySerializationContextPool.Get(options);
context.WriteHeaderPlaceholder();

View File

@ -1,94 +1,94 @@
using System.Collections;
using System.Runtime.CompilerServices;
using AyCode.Core.Helpers;
using static AyCode.Core.Helpers.JsonUtilities;
//using System.Collections;
//using System.Runtime.CompilerServices;
//using AyCode.Core.Helpers;
//using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers;
//namespace AyCode.Core.Serializers;
/// <summary>
/// Helper class for merging IId collections during deserialization.
/// Shared between JSON and Binary deserializers.
/// </summary>
public static class IIdCollectionMergeHelper
{
/// <summary>
/// Builds a lookup dictionary from an existing IId collection.
/// Maps Id values to their corresponding items.
/// </summary>
/// <param name="existingList">The existing collection to index.</param>
/// <param name="idGetter">Function to extract Id from an item.</param>
/// <param name="idType">The type of the Id property.</param>
/// <returns>Dictionary mapping Id to item, or null if collection is empty.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Dictionary<object, object>? BuildIdLookup(
IList existingList,
Func<object, object?> idGetter,
Type idType)
{
var count = existingList.Count;
if (count == 0) return null;
///// <summary>
///// Helper class for merging IId collections during deserialization.
///// Shared between JSON and Binary deserializers.
///// </summary>
//public static class IIdCollectionMergeHelper
//{
// /// <summary>
// /// Builds a lookup dictionary from an existing IId collection.
// /// Maps Id values to their corresponding items.
// /// </summary>
// /// <param name="existingList">The existing collection to index.</param>
// /// <param name="idGetter">Function to extract Id from an item.</param>
// /// <param name="idType">The type of the Id property.</param>
// /// <returns>Dictionary mapping Id to item, or null if collection is empty.</returns>
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public static Dictionary<object, object>? BuildIdLookup(
// IList existingList,
// Func<object, object?> idGetter,
// Type idType)
// {
// var count = existingList.Count;
// if (count == 0) return null;
var dict = new Dictionary<object, object>(count);
for (var i = 0; i < count; i++)
{
var item = existingList[i];
if (item == null) continue;
// var dict = new Dictionary<object, object>(count);
// for (var i = 0; i < count; i++)
// {
// var item = existingList[i];
// if (item == null) continue;
var id = idGetter(item);
if (id != null && !IsDefaultValue(id, idType))
dict[id] = item;
}
return dict;
}
// var id = idGetter(item);
// if (id != null && !IsDefaultValue(id, idType))
// dict[id] = item;
// }
// return dict;
// }
/// <summary>
/// Removes orphaned items from the collection that are not present in the source IDs.
/// </summary>
/// <param name="existingList">The collection to clean up.</param>
/// <param name="existingById">Lookup dictionary of existing items.</param>
/// <param name="sourceIds">Set of IDs that were seen in source data.</param>
public static void RemoveOrphanedItems(
IList existingList,
Dictionary<object, object> existingById,
HashSet<object> sourceIds)
{
var itemsToRemove = new List<object>();
foreach (var kvp in existingById)
{
if (!sourceIds.Contains(kvp.Key))
{
itemsToRemove.Add(kvp.Value);
}
}
// /// <summary>
// /// Removes orphaned items from the collection that are not present in the source IDs.
// /// </summary>
// /// <param name="existingList">The collection to clean up.</param>
// /// <param name="existingById">Lookup dictionary of existing items.</param>
// /// <param name="sourceIds">Set of IDs that were seen in source data.</param>
// public static void RemoveOrphanedItems(
// IList existingList,
// Dictionary<object, object> existingById,
// HashSet<object> sourceIds)
// {
// var itemsToRemove = new List<object>();
// foreach (var kvp in existingById)
// {
// if (!sourceIds.Contains(kvp.Key))
// {
// itemsToRemove.Add(kvp.Value);
// }
// }
foreach (var item in itemsToRemove)
{
existingList.Remove(item);
}
}
// foreach (var item in itemsToRemove)
// {
// existingList.Remove(item);
// }
// }
/// <summary>
/// Copies properties from source object to target object using metadata.
/// </summary>
/// <typeparam name="TPropertyInfo">Type of property info (varies by serializer).</typeparam>
/// <param name="source">Source object to copy from.</param>
/// <param name="target">Target object to copy to.</param>
/// <param name="properties">Array of property accessors.</param>
/// <param name="getter">Function to get property value.</param>
/// <param name="setter">Action to set property value.</param>
public static void CopyProperties<TPropertyInfo>(
object source,
object target,
TPropertyInfo[] properties,
Func<TPropertyInfo, object, object?> getter,
Action<TPropertyInfo, object, object?> setter)
{
for (var i = 0; i < properties.Length; i++)
{
var prop = properties[i];
var value = getter(prop, source);
if (value != null)
setter(prop, target, value);
}
}
}
// /// <summary>
// /// Copies properties from source object to target object using metadata.
// /// </summary>
// /// <typeparam name="TPropertyInfo">Type of property info (varies by serializer).</typeparam>
// /// <param name="source">Source object to copy from.</param>
// /// <param name="target">Target object to copy to.</param>
// /// <param name="properties">Array of property accessors.</param>
// /// <param name="getter">Function to get property value.</param>
// /// <param name="setter">Action to set property value.</param>
// public static void CopyProperties<TPropertyInfo>(
// object source,
// object target,
// TPropertyInfo[] properties,
// Func<TPropertyInfo, object, object?> getter,
// Action<TPropertyInfo, object, object?> setter)
// {
// for (var i = 0; i < properties.Length; i++)
// {
// var prop = properties[i];
// var value = getter(prop, source);
// if (value != null)
// setter(prop, target, value);
// }
// }
//}