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:
parent
f778d4faa9
commit
2eca18ca3f
|
|
@ -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) ===");
|
||||
|
|
|
|||
|
|
@ -25,4 +25,8 @@
|
|||
<Folder Include="Expressions\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="AyCode.Core.Serializers.Console" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
Loading…
Reference in New Issue