[LOADED_DOCS: 3 files, no new loads]
Modernize benchmarks, simplify attributes, doc cleanup - Benchmark output now reports per-op µs and KB/op; added helpers for unit conversion and updated all output formats and headers. - Split SetupAllocBytes into SetupSerializeAllocBytes and SetupDeserializeAllocBytes for finer allocation reporting. - Simplified [AcBinarySerializable] usage in test models to single-argument form. - Edited documentation for clarity, brevity, and consistency; improved navigation, updated technical details, and harmonized terminology across .md files.
This commit is contained in:
parent
4375ca5b4a
commit
3b45de6de3
|
|
@ -113,10 +113,29 @@ public static class Program
|
|||
$"Compression={options.UseCompression}{extra}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a total-time (in ms across <see cref="TestIterations"/>) into per-operation microseconds.
|
||||
/// Formula: <c>totalMs / iterations × 1000</c>. The benchmark stores <c>*TimeMs</c> as the cumulative
|
||||
/// median over the timing run; the display layer renders per-op µs to make numbers iteration-count
|
||||
/// independent (e.g. switching <c>TestIterations</c> 1000 → 100 leaves the displayed µs/op unchanged
|
||||
/// — only its sample noise grows). Symmetric with the already-per-op <c>*AllocBytesPerOp</c> fields.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ToPerOpMicros(double totalMs) => totalMs / TestIterations * 1000.0;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a byte count to KB (1 KB = 1024 B). Display-only helper so allocation columns can
|
||||
/// render compact F2 KB values (e.g. <c>4.05 KB</c> instead of <c>4,144 B</c>) — header carries
|
||||
/// the unit so per-row entries stay numbers-only. CSV / raw-data outputs keep the precise byte
|
||||
/// integers untouched.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ToKilobytes(long bytes) => bytes / 1024.0;
|
||||
|
||||
#if DEBUG
|
||||
private const int WarmupIterations = 0;
|
||||
private const int TestIterations = 1;
|
||||
private const int BenchmarkSamples = 1; // Debug: single sample, fast iteration
|
||||
private static int WarmupIterations = 0;
|
||||
private static int TestIterations = 1;
|
||||
private static int BenchmarkSamples = 1; // Debug: single sample, fast iteration
|
||||
#else
|
||||
private static int WarmupIterations = 5000; //5000
|
||||
private static int TestIterations = 1000; //1000
|
||||
|
|
@ -330,6 +349,7 @@ public static class Program
|
|||
{
|
||||
System.Console.Error.WriteLine($"❌ FATAL: Round-trip verification FAILED for {serializer.Name} on {testData.DisplayName}");
|
||||
System.Console.Error.WriteLine("Benchmark numbers from a serializer with broken round-trip would be meaningless. Aborting.");
|
||||
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -360,7 +380,8 @@ public static class Program
|
|||
OptionsPreset = serializer.OptionsPreset,
|
||||
OptionsDescription = serializer.OptionsDescription,
|
||||
SerializedSize = serializer.SerializedSize,
|
||||
SetupAllocBytes = serializer.SetupAllocBytes,
|
||||
SetupSerializeAllocBytes = serializer.SetupSerializeAllocBytes,
|
||||
SetupDeserializeAllocBytes = serializer.SetupDeserializeAllocBytes,
|
||||
IsRoundTripOnly = serializer.IsRoundTripOnly
|
||||
};
|
||||
|
||||
|
|
@ -480,7 +501,7 @@ public static class Program
|
|||
// AcBinary via IBufferWriter (FRESH ArrayBufferWriter per call — one-shot scenario).
|
||||
// 4 KB chunk size from binaryFastModeBufWrChunk — minimises the per-call ArrayBufferWriter
|
||||
// allocation. Optimum for this scenario.
|
||||
new AcBinaryFreshBufferWriterBenchmark(testData.Order, binaryFastModeBufWrChunk, "FastMode (4KB BufWr)"),
|
||||
new AcBinaryFreshBufferWriterBenchmark(testData.Order, binaryFastModeBufWrChunk, "FastMode (4KB)"),
|
||||
|
||||
// AsyncPipe streaming (AcBinaryNamedPipeBenchmark) is intentionally OMITTED here — run it via
|
||||
// the dedicated AsyncPipe menu / CLI mode for isolated streaming-I/O measurements.
|
||||
|
|
@ -695,8 +716,10 @@ public static class Program
|
|||
string Name => $"{Engine} ({IoMode}, {OptionsPreset})";
|
||||
int SerializedSize { get; }
|
||||
string? OptionsDescription => null;
|
||||
/// <summary>One-time setup allocation cost (e.g., pre-allocated ArrayBufferWriter with internal buffer). Captured in constructor; 0 for byte[] API and Fresh-BufWriter variants.</summary>
|
||||
long SetupAllocBytes { get; }
|
||||
/// <summary>One-time SERIALIZER-side setup allocation cost (e.g., pre-allocated ArrayBufferWriter with internal buffer). Captured in constructor; 0 for byte[] API and Fresh-BufWriter variants.</summary>
|
||||
long SetupSerializeAllocBytes { get; }
|
||||
/// <summary>One-time DESERIALIZER-side setup allocation cost (e.g., long-lived AsyncPipeReaderInput's ArrayPool rent + ManualResetEventSlim, drain-task scaffolding). Captured in constructor; 0 for byte[] API and any setup-free deserialize path.</summary>
|
||||
long SetupDeserializeAllocBytes { get; }
|
||||
/// <summary>True when Serialize() does a full round-trip (e.g. NamedPipe) and Deserialize() is a no-op.
|
||||
/// Used by the SUMMARY: WINNERS section to skip such cells from "Fastest Serialize" and "Fastest Deserialize"
|
||||
/// rankings (because both metrics are misleading there) — they still participate in "Fastest Round-trip".
|
||||
|
|
@ -720,7 +743,8 @@ public static class Program
|
|||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
||||
public string OptionsPreset { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
public long SetupAllocBytes => 0;
|
||||
public long SetupSerializeAllocBytes => 0;
|
||||
public long SetupDeserializeAllocBytes => 0;
|
||||
public string OptionsDescription => BuildAcBinaryOptionsDescription(_options);
|
||||
|
||||
public AcBinaryBenchmark(TestOrder order, AcBinarySerializerOptions options, string optionsPreset)
|
||||
|
|
@ -775,7 +799,8 @@ public static class Program
|
|||
public string DispatchMode => ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters
|
||||
public string OptionsPreset { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
public long SetupAllocBytes => 0;
|
||||
public long SetupSerializeAllocBytes => 0;
|
||||
public long SetupDeserializeAllocBytes => 0;
|
||||
|
||||
public MemoryPackBenchmark(TestOrder order, string optionsPreset)
|
||||
{
|
||||
|
|
@ -818,7 +843,8 @@ public static class Program
|
|||
public string DispatchMode => ModeSGen; // MessagePack uses [MessagePackObject] source-generated formatters (StandardResolver)
|
||||
public string OptionsPreset { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
public long SetupAllocBytes => 0;
|
||||
public long SetupSerializeAllocBytes => 0;
|
||||
public long SetupDeserializeAllocBytes => 0;
|
||||
public string OptionsDescription { get; }
|
||||
|
||||
public MessagePackBenchmark(TestOrder order, string optionsPreset)
|
||||
|
|
@ -881,7 +907,8 @@ public static class Program
|
|||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
||||
public string OptionsPreset { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
public long SetupAllocBytes => 0;
|
||||
public long SetupSerializeAllocBytes => 0;
|
||||
public long SetupDeserializeAllocBytes => 0;
|
||||
public string OptionsDescription => BuildAcBinaryOptionsDescription(_options, $", BufferSize={_options.BufferWriterChunkSize}B");
|
||||
|
||||
public AcBinaryFreshBufferWriterBenchmark(TestOrder order, AcBinarySerializerOptions options, string optionsPreset)
|
||||
|
|
@ -978,7 +1005,8 @@ public static class Program
|
|||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
||||
public string OptionsPreset { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
public long SetupAllocBytes => 0;
|
||||
public long SetupSerializeAllocBytes { get; }
|
||||
public long SetupDeserializeAllocBytes { get; }
|
||||
public bool IsRoundTripOnly => true;
|
||||
public string OptionsDescription => BuildAcBinaryOptionsDescription(_options, $", BufferSize={_options.BufferWriterChunkSize}B, Transport=NamedPipe(long-lived,multiMessage)");
|
||||
|
||||
|
|
@ -1000,30 +1028,41 @@ public static class Program
|
|||
// no fragmentation, no IRP reordering. _options.BufferWriterChunkSize is the single tunable source.
|
||||
var pipeName = $"AcBinaryBench-{Guid.NewGuid():N}";
|
||||
|
||||
// === SERIALIZE-side setup measurement ===
|
||||
// pipe-pair (server + client) + connect handshake + writer-side PipeWriter wrapper.
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
var beforeSer = GC.GetAllocatedBytesForCurrentThread();
|
||||
_pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte,
|
||||
System.IO.Pipes.PipeOptions.Asynchronous,
|
||||
inBufferSize: _options.BufferWriterChunkSize,
|
||||
outBufferSize: _options.BufferWriterChunkSize);
|
||||
|
||||
_pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.Out, System.IO.Pipes.PipeOptions.Asynchronous);
|
||||
|
||||
var serverWait = _pipeServer.WaitForConnectionAsync();
|
||||
_pipeClient.Connect();
|
||||
serverWait.GetAwaiter().GetResult();
|
||||
|
||||
_pipeWriter = PipeWriter.Create(_pipeClient);
|
||||
_pipeReader = PipeReader.Create(_pipeServer);
|
||||
var afterSer = GC.GetAllocatedBytesForCurrentThread();
|
||||
SetupSerializeAllocBytes = afterSer - beforeSer;
|
||||
|
||||
// === DESERIALIZE-side setup measurement ===
|
||||
// PipeReader wrapper + AsyncPipeReaderInput (ArrayPool rent + ManualResetEventSlim) + drain
|
||||
// task scaffolding. The long-lived deserialize-side allocation that per-iter measurements
|
||||
// hide today, surfaced here for comparison-vs-FreshInstance fairness.
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
var beforeDes = GC.GetAllocatedBytesForCurrentThread();
|
||||
_pipeReader = PipeReader.Create(_pipeServer);
|
||||
// 1× multi-message receive infrastructure: long-lived input + 1 background drain task.
|
||||
// Per-iter Serialize() does its own Deserialize<T>(input, opts) call on the calling thread —
|
||||
// strictly sequential per the calling thread's loop, so the producer (drain) and consumer
|
||||
// (deserialiser, on the calling thread) cannot race on the buffer.
|
||||
_input = new AsyncPipeReaderInput(_options.BufferWriterChunkSize * 2, multiMessage: true);
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
// Drain task: pumps PipeReader → input.Feed forever (or until cancel). Single Task.Run for
|
||||
// the full benchmark lifetime (NOT per iteration) — its overhead is amortised across all messages.
|
||||
_drainTask = Task.Run(() => _input.DrainFromAsync(_pipeReader, _cts.Token));
|
||||
var afterDes = GC.GetAllocatedBytesForCurrentThread();
|
||||
SetupDeserializeAllocBytes = afterDes - beforeDes;
|
||||
}
|
||||
|
||||
public void Warmup(int iterations)
|
||||
|
|
@ -1096,7 +1135,8 @@ public static class Program
|
|||
public string DispatchMode => ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters
|
||||
public string OptionsPreset { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
public long SetupAllocBytes => 0;
|
||||
public long SetupSerializeAllocBytes => 0;
|
||||
public long SetupDeserializeAllocBytes => 0;
|
||||
|
||||
public MemoryPackFreshBufferWriterBenchmark(TestOrder order, string optionsPreset)
|
||||
{
|
||||
|
|
@ -1145,7 +1185,8 @@ public static class Program
|
|||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
||||
public string OptionsPreset { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
public long SetupAllocBytes { get; }
|
||||
public long SetupSerializeAllocBytes { get; }
|
||||
public long SetupDeserializeAllocBytes => 0;
|
||||
public string OptionsDescription => BuildAcBinaryOptionsDescription(_options);
|
||||
|
||||
public AcBinaryBufferWriterBenchmark(TestOrder order, AcBinarySerializerOptions options, string optionsPreset)
|
||||
|
|
@ -1155,12 +1196,14 @@ public static class Program
|
|||
OptionsPreset = optionsPreset;
|
||||
_serialized = AcBinarySerializer.Serialize(order, options);
|
||||
|
||||
// Measure ONLY the BufferWriter infrastructure setup (excluding the helper Serialize above)
|
||||
// Measure ONLY the BufferWriter infrastructure setup on the serialize side (excluding the
|
||||
// helper Serialize above). Deserialize side reads directly from `_serialized` byte[] — no
|
||||
// dedicated setup allocation, hence SetupDeserializeAllocBytes = 0.
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
var beforeSetup = GC.GetAllocatedBytesForCurrentThread();
|
||||
_bufferWriter = new ArrayBufferWriter<byte>(_serialized.Length * 2);
|
||||
var afterSetup = GC.GetAllocatedBytesForCurrentThread();
|
||||
SetupAllocBytes = afterSetup - beforeSetup;
|
||||
SetupSerializeAllocBytes = afterSetup - beforeSetup;
|
||||
}
|
||||
|
||||
public void Warmup(int iterations)
|
||||
|
|
@ -1206,7 +1249,8 @@ public static class Program
|
|||
public string DispatchMode => ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters
|
||||
public string OptionsPreset { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
public long SetupAllocBytes { get; }
|
||||
public long SetupSerializeAllocBytes { get; }
|
||||
public long SetupDeserializeAllocBytes => 0;
|
||||
|
||||
public MemoryPackBufferWriterBenchmark(TestOrder order, string optionsPreset)
|
||||
{
|
||||
|
|
@ -1214,11 +1258,12 @@ public static class Program
|
|||
OptionsPreset = optionsPreset;
|
||||
_serialized = MemoryPackSerializer.Serialize(order);
|
||||
|
||||
// Serialize-side setup only — see AcBinaryBufferWriterBenchmark for the full rationale.
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
var beforeSetup = GC.GetAllocatedBytesForCurrentThread();
|
||||
_bufferWriter = new ArrayBufferWriter<byte>(_serialized.Length * 2);
|
||||
var afterSetup = GC.GetAllocatedBytesForCurrentThread();
|
||||
SetupAllocBytes = afterSetup - beforeSetup;
|
||||
SetupSerializeAllocBytes = afterSetup - beforeSetup;
|
||||
}
|
||||
|
||||
public void Warmup(int iterations)
|
||||
|
|
@ -1261,7 +1306,8 @@ public static class Program
|
|||
public string DispatchMode => ModeRuntime; // System.Text.Json default uses reflection-based metadata (no source generator opt-in here)
|
||||
public string OptionsPreset { get; }
|
||||
public int SerializedSize => _serializedUtf8.Length;
|
||||
public long SetupAllocBytes => 0;
|
||||
public long SetupSerializeAllocBytes => 0;
|
||||
public long SetupDeserializeAllocBytes => 0;
|
||||
|
||||
public SystemTextJsonBenchmark(TestOrder order, string optionsPreset)
|
||||
{
|
||||
|
|
@ -1313,8 +1359,8 @@ public static class Program
|
|||
public string OptionsPreset { get; set; } = "";
|
||||
/// <summary>True if Serialize() captures a full round-trip and Deserialize() is a no-op
|
||||
/// (single-use streaming transports like NamedPipe). Excluded from "Fastest Serialize" / "Fastest Deserialize"
|
||||
/// winners rankings; still ranked in "Fastest Round-trip". Display-side: Ser ms / SerAlloc / Des ms / DesAlloc
|
||||
/// all show "N/A" since they were never measured separately; RT ms / RT Alloc carry the full round-trip values.</summary>
|
||||
/// winners rankings; still ranked in "Fastest Round-trip". Display-side: Ser µs/op / SerAlloc / Des µs/op / DesAlloc
|
||||
/// all show "N/A" since they were never measured separately; RT µs/op / RT Alloc carry the full round-trip values.</summary>
|
||||
public bool IsRoundTripOnly { get; set; }
|
||||
/// <summary>Synthesized display name for backwards compatibility / single-string-row scenarios. Includes DispatchMode so SGen and Runtime variants of the same preset don't collide in grouping (e.g. SUMMARY: WINNERS).</summary>
|
||||
public string SerializerName => $"{Engine} ({IoMode}, {OptionsPreset}, {DispatchMode})";
|
||||
|
|
@ -1324,7 +1370,8 @@ public static class Program
|
|||
public double DeserializeTimeMs { get; set; }
|
||||
public long SerializeAllocBytesPerOp { get; set; }
|
||||
public long DeserializeAllocBytesPerOp { get; set; }
|
||||
public long SetupAllocBytes { get; set; }
|
||||
public long SetupSerializeAllocBytes { get; set; }
|
||||
public long SetupDeserializeAllocBytes { get; set; }
|
||||
/// <summary>Total round-trip time. For in-memory benchmarks: <c>Serialize + Deserialize</c> (set explicitly in
|
||||
/// <c>RunBenchmarksForTestData</c>). For round-trip-only benchmarks (NamedPipe etc.): the directly-measured
|
||||
/// pipe round-trip time, since Ser and Des are not separately measurable there.</summary>
|
||||
|
|
@ -1337,11 +1384,12 @@ public static class Program
|
|||
|
||||
private static void PrintResult(BenchmarkResult result)
|
||||
{
|
||||
var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs,8:F2} ms" : " N/A";
|
||||
var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs,8:F2} ms" : " N/A";
|
||||
var serAlloc = result.SerializeTimeMs > 0 ? $"{result.SerializeAllocBytesPerOp,8:N0} B/op" : " N/A";
|
||||
var desAlloc = result.DeserializeTimeMs > 0 ? $"{result.DeserializeAllocBytesPerOp,8:N0} B/op" : " N/A";
|
||||
System.Console.WriteLine($" {result.SerializerName,-40} | Size: {result.SerializedSize,8:N0} | Ser: {ser} ({serAlloc}) | Des: {des} ({desAlloc})");
|
||||
// Numbers-only per-row entries; the column-headers carry units (µs/op, KB/op).
|
||||
var ser = result.SerializeTimeMs > 0 ? $"{ToPerOpMicros(result.SerializeTimeMs),7:F2}" : " N/A";
|
||||
var des = result.DeserializeTimeMs > 0 ? $"{ToPerOpMicros(result.DeserializeTimeMs),7:F2}" : " N/A";
|
||||
var serAlloc = result.SerializeTimeMs > 0 ? $"{ToKilobytes(result.SerializeAllocBytesPerOp),7:F2}" : " N/A";
|
||||
var desAlloc = result.DeserializeTimeMs > 0 ? $"{ToKilobytes(result.DeserializeAllocBytesPerOp),7:F2}" : " N/A";
|
||||
System.Console.WriteLine($" {result.SerializerName,-40} | Size: {result.SerializedSize,8:N0} B | Ser: {ser} µs/op ({serAlloc} KB/op) | Des: {des} µs/op ({desAlloc} KB/op)");
|
||||
}
|
||||
|
||||
private static void PrintGroupedResults(List<BenchmarkResult> results, List<TestDataSet> testDataSets)
|
||||
|
|
@ -1375,20 +1423,21 @@ public static class Program
|
|||
var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen));
|
||||
|
||||
System.Console.WriteLine($"\n┌─ {testData.DisplayName} ─".PadRight(172, '─') + "┐");
|
||||
System.Console.WriteLine($"│ {"#",-4} │ {"Engine",-11} │ {"Options",-22} │ {"IO",-12} │ {"Mode",-8} │ {"Setup",-8} │ {"Size",-8} │ {"Ser ms",-10} │ {"SerAlloc",-10} │ {"Des ms",-10} │ {"DesAlloc",-10} │ {"RT ms",-10} │ {"RT Alloc",-10} │");
|
||||
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┼{"─".PadRight(13, '─')}┼{"─".PadRight(24, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(10, '─')}┼{"─".PadRight(10, '─')}┼{"─".PadRight(10, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┤");
|
||||
// Header-only units; per-row entries are numbers (µs/op for time, KB/op for alloc, KB pair "ser / des" for Setup, B for Size).
|
||||
System.Console.WriteLine($"│ {"#",-4} │ {"Engine",-11} │ {"Options",-22} │ {"IO",-12} │ {"Mode",-8} │ {"Setup S/D KB",-14} │ {"Size B",-8} │ {"Ser µs/op",-10} │ {"SerAlc KB",-10} │ {"Des µs/op",-10} │ {"DesAlc KB",-10} │ {"RT µs/op",-10} │ {"RTAlc KB",-10} │");
|
||||
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┼{"─".PadRight(13, '─')}┼{"─".PadRight(24, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(10, '─')}┼{"─".PadRight(16, '─')}┼{"─".PadRight(10, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┤");
|
||||
|
||||
var rank = 1;
|
||||
foreach (var result in testResults)
|
||||
{
|
||||
var size = $"{result.SerializedSize:N0}";
|
||||
var setup = result.SetupAllocBytes > 0 ? $"{result.SetupAllocBytes:N0}" : "0";
|
||||
var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs:F2} ms" : "N/A";
|
||||
var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs:F2} ms" : "N/A";
|
||||
var rt = result.RoundTripTimeMs > 0 ? $"{result.RoundTripTimeMs:F2} ms" : "N/A";
|
||||
var serAlloc = result.SerializeTimeMs > 0 ? $"{result.SerializeAllocBytesPerOp:N0} B" : "N/A";
|
||||
var desAlloc = result.DeserializeTimeMs > 0 ? $"{result.DeserializeAllocBytesPerOp:N0} B" : "N/A";
|
||||
var rtAlloc = result.RoundTripAllocBytesPerOp > 0 ? $"{result.RoundTripAllocBytesPerOp:N0} B" : "N/A";
|
||||
var setup = $"{ToKilobytes(result.SetupSerializeAllocBytes):F2} / {ToKilobytes(result.SetupDeserializeAllocBytes):F2}";
|
||||
var ser = result.SerializeTimeMs > 0 ? $"{ToPerOpMicros(result.SerializeTimeMs):F2}" : "N/A";
|
||||
var des = result.DeserializeTimeMs > 0 ? $"{ToPerOpMicros(result.DeserializeTimeMs):F2}" : "N/A";
|
||||
var rt = result.RoundTripTimeMs > 0 ? $"{ToPerOpMicros(result.RoundTripTimeMs):F2}" : "N/A";
|
||||
var serAlloc = result.SerializeTimeMs > 0 ? $"{ToKilobytes(result.SerializeAllocBytesPerOp):F2}" : "N/A";
|
||||
var desAlloc = result.DeserializeTimeMs > 0 ? $"{ToKilobytes(result.DeserializeAllocBytesPerOp):F2}" : "N/A";
|
||||
var rtAlloc = result.RoundTripAllocBytesPerOp > 0 ? $"{ToKilobytes(result.RoundTripAllocBytesPerOp):F2}" : "N/A";
|
||||
|
||||
// Highlight MemoryPack baseline (any Byte[]) and AcBinary headline contender (Byte[] + SGen) with win/lose colors.
|
||||
// The AcBinary Byte[]+Runtime variant is shown unhighlighted — it's contextual (SGen speed-up reference), not the headline.
|
||||
|
|
@ -1413,7 +1462,7 @@ public static class Program
|
|||
}
|
||||
}
|
||||
|
||||
System.Console.WriteLine($"{prefix}{rank++,4} │ {result.Engine,-11} │ {result.OptionsPreset,-22} │ {result.IoMode,-12} │ {result.DispatchMode,-8} │ {setup,8} │ {size,8} │ {ser,10} │ {serAlloc,10} │ {des,10} │ {desAlloc,10} │ {rt,10} │ {rtAlloc,10}{suffix}");
|
||||
System.Console.WriteLine($"{prefix}{rank++,4} │ {result.Engine,-11} │ {result.OptionsPreset,-22} │ {result.IoMode,-12} │ {result.DispatchMode,-8} │ {setup,14} │ {size,8} │ {ser,10} │ {serAlloc,10} │ {des,10} │ {desAlloc,10} │ {rt,10} │ {rtAlloc,10}{suffix}");
|
||||
|
||||
if (isHighlighted)
|
||||
{
|
||||
|
|
@ -1433,13 +1482,13 @@ public static class Program
|
|||
var rtAllocPct = memPackResult.RoundTripAllocBytesPerOp > 0 ? (acBinaryResult.RoundTripAllocBytesPerOp / (double)memPackResult.RoundTripAllocBytesPerOp - 1) * 100 : 0;
|
||||
|
||||
// Footer separator: merge first 5 cols (#, Engine, Options, IO, Mode) → comparison label;
|
||||
// remaining 8 cols stay aligned (Setup, Size, Ser ms, SerAlloc, Des ms, DesAlloc, RT ms, RT Alloc).
|
||||
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┴{"─".PadRight(13, '─')}┴{"─".PadRight(24, '─')}┴{"─".PadRight(14, '─')}┴{"─".PadRight(10, '─')}┼{"─".PadRight(10, '─')}┼{"─".PadRight(10, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┤");
|
||||
// remaining 8 cols stay aligned (Setup S/D KB, Size, Ser µs/op, SerAlc KB, Des µs/op, DesAlc KB, RT µs/op, RTAlc KB).
|
||||
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┴{"─".PadRight(13, '─')}┴{"─".PadRight(24, '─')}┴{"─".PadRight(14, '─')}┴{"─".PadRight(10, '─')}┼{"─".PadRight(16, '─')}┼{"─".PadRight(10, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(12, '─')}┤");
|
||||
// Merged label cell width = 4 + 11 + 22 + 12 + 8 + 4*3 (dropped separators) = 69
|
||||
System.Console.Write($"│ {"► AcBinary (Byte[]) vs MemoryPack (Byte[])",-69} │ ");
|
||||
|
||||
// Setup (n/a for Byte[] vs Byte[] — neither pre-allocates)
|
||||
System.Console.Write($"{"—",8}");
|
||||
// Setup S/D KB (n/a for Byte[] vs Byte[] — neither pre-allocates)
|
||||
System.Console.Write($"{"—",14}");
|
||||
System.Console.Write(" │ ");
|
||||
|
||||
// Size
|
||||
|
|
@ -1486,7 +1535,7 @@ public static class Program
|
|||
}
|
||||
|
||||
// Closing line: merged on left (─ between cols 1-5), ┴ on the right (cols 6-13 boundary, 8 unmerged cells).
|
||||
System.Console.WriteLine($"└{"─".PadRight(6, '─')}─{"─".PadRight(13, '─')}─{"─".PadRight(24, '─')}─{"─".PadRight(14, '─')}─{"─".PadRight(10, '─')}┴{"─".PadRight(10, '─')}┴{"─".PadRight(10, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┘");
|
||||
System.Console.WriteLine($"└{"─".PadRight(6, '─')}─{"─".PadRight(13, '─')}─{"─".PadRight(24, '─')}─{"─".PadRight(14, '─')}─{"─".PadRight(10, '─')}┴{"─".PadRight(16, '─')}┴{"─".PadRight(10, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(12, '─')}┘");
|
||||
//System.Console.WriteLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
||||
//System.Console.WriteLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
|
||||
}
|
||||
|
|
@ -1508,7 +1557,7 @@ public static class Program
|
|||
.OrderBy(x => x.AvgTime)
|
||||
.FirstOrDefault();
|
||||
if (fastestSer != null)
|
||||
System.Console.WriteLine($"{"Fastest Serialize",-20} │ {fastestSer.Name,-40} │ {fastestSer.AvgTime,15:F2} ms");
|
||||
System.Console.WriteLine($"{"Fastest Serialize",-20} │ {fastestSer.Name,-40} │ {ToPerOpMicros(fastestSer.AvgTime),12:F2} µs/op");
|
||||
|
||||
// Fastest Deserialize — round-trip-only serializers excluded (their Deserialize() is a no-op).
|
||||
var fastestDes = results.Where(r => r.DeserializeTimeMs > 0 && !r.IsRoundTripOnly)
|
||||
|
|
@ -1517,7 +1566,7 @@ public static class Program
|
|||
.OrderBy(x => x.AvgTime)
|
||||
.FirstOrDefault();
|
||||
if (fastestDes != null)
|
||||
System.Console.WriteLine($"{"Fastest Deserialize",-20} │ {fastestDes.Name,-40} │ {fastestDes.AvgTime,15:F2} ms");
|
||||
System.Console.WriteLine($"{"Fastest Deserialize",-20} │ {fastestDes.Name,-40} │ {ToPerOpMicros(fastestDes.AvgTime),12:F2} µs/op");
|
||||
|
||||
// Smallest Size
|
||||
var smallestSize = results
|
||||
|
|
@ -1535,7 +1584,7 @@ public static class Program
|
|||
.OrderBy(x => x.AvgTime)
|
||||
.FirstOrDefault();
|
||||
if (fastestRt != null)
|
||||
System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-40} │ {fastestRt.AvgTime,15:F2} ms");
|
||||
System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-40} │ {ToPerOpMicros(fastestRt.AvgTime),12:F2} µs/op");
|
||||
|
||||
// Overall AcBinary (SGen) vs MemoryPack comparison (baseline switched MessagePack → MemoryPack as SOTA reference).
|
||||
// AcBinary side is restricted to DispatchMode == SGen — apples-to-apples vs MemoryPack which is also source-generated.
|
||||
|
|
@ -1579,7 +1628,7 @@ public static class Program
|
|||
{
|
||||
var serPctAll = (acBinaryAvgSer / memPackAvgSer - 1) * 100;
|
||||
System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {memPackAvgSer:F2} ms)");
|
||||
System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({ToPerOpMicros(acBinaryAvgSer):F2} µs/op vs {ToPerOpMicros(memPackAvgSer):F2} µs/op)");
|
||||
System.Console.ResetColor();
|
||||
}
|
||||
|
||||
|
|
@ -1588,11 +1637,11 @@ public static class Program
|
|||
var sizePctAll = (acBinaryAvgSize / memPackAvgSize - 1) * 100;
|
||||
|
||||
System.Console.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {memPackAvgDes:F2} ms)");
|
||||
System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({ToPerOpMicros(acBinaryAvgDes):F2} µs/op vs {ToPerOpMicros(memPackAvgDes):F2} µs/op)");
|
||||
System.Console.ResetColor();
|
||||
|
||||
System.Console.ForegroundColor = rtPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.WriteLine($" Round-trip: {rtPctAll:+0;-0}% ({acBinaryAvgRt:F2} ms vs {memPackAvgRt:F2} ms)");
|
||||
System.Console.WriteLine($" Round-trip: {rtPctAll:+0;-0}% ({ToPerOpMicros(acBinaryAvgRt):F2} µs/op vs {ToPerOpMicros(memPackAvgRt):F2} µs/op)");
|
||||
System.Console.ResetColor();
|
||||
|
||||
System.Console.ForegroundColor = sizePctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
|
|
@ -1673,15 +1722,15 @@ public static class Program
|
|||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// CSV-like data for easy import (now includes per-op allocation columns)
|
||||
// CSV-like data for easy import — keeps raw byte integers (no KB rounding) so external tools can compute precisely.
|
||||
sb.AppendLine("=== RAW DATA (CSV) ===");
|
||||
sb.AppendLine("TestData,Engine,IO,Mode,Options,Size,SerializeMs,DeserializeMs,RoundTripMs,SerializeAllocBytesPerOp,DeserializeAllocBytesPerOp,RoundTripAllocBytesPerOp,SetupAllocBytes");
|
||||
sb.AppendLine("TestData,Engine,IO,Mode,Options,Size,SerializeMicrosPerOp,DeserializeMicrosPerOp,RoundTripMicrosPerOp,SerializeAllocBytesPerOp,DeserializeAllocBytesPerOp,RoundTripAllocBytesPerOp,SetupSerializeAllocBytes,SetupDeserializeAllocBytes");
|
||||
foreach (var testData in testDataSets)
|
||||
{
|
||||
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList();
|
||||
foreach (var result in testResults)
|
||||
{
|
||||
sb.AppendLine($"{result.TestDataName},{result.Engine},{result.IoMode},{result.DispatchMode},{result.OptionsPreset},{result.SerializedSize},{result.SerializeTimeMs:F2},{result.DeserializeTimeMs:F2},{result.RoundTripTimeMs:F2},{result.SerializeAllocBytesPerOp},{result.DeserializeAllocBytesPerOp},{result.RoundTripAllocBytesPerOp},{result.SetupAllocBytes}");
|
||||
sb.AppendLine($"{result.TestDataName},{result.Engine},{result.IoMode},{result.DispatchMode},{result.OptionsPreset},{result.SerializedSize},{ToPerOpMicros(result.SerializeTimeMs):F2},{ToPerOpMicros(result.DeserializeTimeMs):F2},{ToPerOpMicros(result.RoundTripTimeMs):F2},{result.SerializeAllocBytesPerOp},{result.DeserializeAllocBytesPerOp},{result.RoundTripAllocBytesPerOp},{result.SetupSerializeAllocBytes},{result.SetupDeserializeAllocBytes}");
|
||||
}
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
|
@ -1701,8 +1750,8 @@ public static class Program
|
|||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"--- {testData.DisplayName} ---");
|
||||
sb.AppendLine($"{"#",-4} {"Serializer",-42} {"Size",-12} {"Serialize",-14} {"Deserialize",-14} {"Round-trip",-14} {"SerAlloc",-12} {"DesAlloc",-12}");
|
||||
sb.AppendLine(new string('-', 130));
|
||||
sb.AppendLine($"{"#",-4} {"Serializer",-42} {"Size B",-12} {"Setup S/D KB",-14} {"Ser µs/op",-12} {"Des µs/op",-12} {"RT µs/op",-12} {"SerAlc KB",-11} {"DesAlc KB",-11}");
|
||||
sb.AppendLine(new string('-', 140));
|
||||
|
||||
var rank = 1;
|
||||
foreach (var result in testResults)
|
||||
|
|
@ -1711,13 +1760,14 @@ public static class Program
|
|||
var prefix = isHighlighted ? "► " : " ";
|
||||
|
||||
var size = $"{result.SerializedSize:N0}";
|
||||
var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs:F2} ms" : "N/A";
|
||||
var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs:F2} ms" : "N/A";
|
||||
var rt = result.RoundTripTimeMs > 0 ? $"{result.RoundTripTimeMs:F2} ms" : "N/A";
|
||||
var serAlloc = result.SerializeTimeMs > 0 ? $"{result.SerializeAllocBytesPerOp:N0} B" : "N/A";
|
||||
var desAlloc = result.DeserializeTimeMs > 0 ? $"{result.DeserializeAllocBytesPerOp:N0} B" : "N/A";
|
||||
var setup = $"{ToKilobytes(result.SetupSerializeAllocBytes):F2} / {ToKilobytes(result.SetupDeserializeAllocBytes):F2}";
|
||||
var ser = result.SerializeTimeMs > 0 ? $"{ToPerOpMicros(result.SerializeTimeMs):F2}" : "N/A";
|
||||
var des = result.DeserializeTimeMs > 0 ? $"{ToPerOpMicros(result.DeserializeTimeMs):F2}" : "N/A";
|
||||
var rt = result.RoundTripTimeMs > 0 ? $"{ToPerOpMicros(result.RoundTripTimeMs):F2}" : "N/A";
|
||||
var serAlloc = result.SerializeTimeMs > 0 ? $"{ToKilobytes(result.SerializeAllocBytesPerOp):F2}" : "N/A";
|
||||
var desAlloc = result.DeserializeTimeMs > 0 ? $"{ToKilobytes(result.DeserializeAllocBytesPerOp):F2}" : "N/A";
|
||||
|
||||
sb.AppendLine($"{rank++,2} {prefix}{result.SerializerName,-40} {size,-12} {ser,-14} {des,-14} {rt,-14} {serAlloc,-12} {desAlloc,-12}");
|
||||
sb.AppendLine($"{rank++,2} {prefix}{result.SerializerName,-40} {size,-12} {setup,-14} {ser,-12} {des,-12} {rt,-12} {serAlloc,-11} {desAlloc,-11}");
|
||||
}
|
||||
|
||||
// Summary row for this test data (vs MemoryPack — baseline switched MessagePack → MemoryPack)
|
||||
|
|
@ -1756,7 +1806,7 @@ public static class Program
|
|||
var acBinaryAvgSer2 = acBinarySerResults2.Average(r => r.SerializeTimeMs);
|
||||
var memPackAvgSerAlloc2 = memPackSerResults2.Average(r => r.SerializeAllocBytesPerOp);
|
||||
var acBinaryAvgSerAlloc2 = acBinarySerResults2.Average(r => r.SerializeAllocBytesPerOp);
|
||||
sb.AppendLine($" Serialize: {((acBinaryAvgSer2 / memPackAvgSer2 - 1) * 100):+0;-0}% ({acBinaryAvgSer2:F2} ms vs {memPackAvgSer2:F2} ms)");
|
||||
sb.AppendLine($" Serialize: {((acBinaryAvgSer2 / memPackAvgSer2 - 1) * 100):+0;-0}% ({ToPerOpMicros(acBinaryAvgSer2):F2} µs/op vs {ToPerOpMicros(memPackAvgSer2):F2} µs/op)");
|
||||
if (memPackAvgSerAlloc2 > 0)
|
||||
sb.AppendLine($" Ser Alloc: {((acBinaryAvgSerAlloc2 / memPackAvgSerAlloc2 - 1) * 100):+0;-0}% ({acBinaryAvgSerAlloc2:F0} B/op vs {memPackAvgSerAlloc2:F0} B/op)");
|
||||
}
|
||||
|
|
@ -1767,7 +1817,7 @@ public static class Program
|
|||
var acBinaryAvgDes2 = acBinaryDesResults2.Average(r => r.DeserializeTimeMs);
|
||||
var memPackAvgDesAlloc2 = memPackDesResults2.Average(r => r.DeserializeAllocBytesPerOp);
|
||||
var acBinaryAvgDesAlloc2 = acBinaryDesResults2.Average(r => r.DeserializeAllocBytesPerOp);
|
||||
sb.AppendLine($" Deserialize: {((acBinaryAvgDes2 / memPackAvgDes2 - 1) * 100):+0;-0}% ({acBinaryAvgDes2:F2} ms vs {memPackAvgDes2:F2} ms)");
|
||||
sb.AppendLine($" Deserialize: {((acBinaryAvgDes2 / memPackAvgDes2 - 1) * 100):+0;-0}% ({ToPerOpMicros(acBinaryAvgDes2):F2} µs/op vs {ToPerOpMicros(memPackAvgDes2):F2} µs/op)");
|
||||
if (memPackAvgDesAlloc2 > 0)
|
||||
sb.AppendLine($" Des Alloc: {((acBinaryAvgDesAlloc2 / memPackAvgDesAlloc2 - 1) * 100):+0;-0}% ({acBinaryAvgDesAlloc2:F0} B/op vs {memPackAvgDesAlloc2:F0} B/op)");
|
||||
}
|
||||
|
|
@ -1776,7 +1826,7 @@ public static class Program
|
|||
{
|
||||
var memPackAvgRt2 = memPackRtResults2.Average(r => r.RoundTripTimeMs);
|
||||
var acBinaryAvgRt2 = acBinaryRtResults2.Average(r => r.RoundTripTimeMs);
|
||||
sb.AppendLine($" Round-trip: {((acBinaryAvgRt2 / memPackAvgRt2 - 1) * 100):+0;-0}% ({acBinaryAvgRt2:F2} ms vs {memPackAvgRt2:F2} ms)");
|
||||
sb.AppendLine($" Round-trip: {((acBinaryAvgRt2 / memPackAvgRt2 - 1) * 100):+0;-0}% ({ToPerOpMicros(acBinaryAvgRt2):F2} µs/op vs {ToPerOpMicros(memPackAvgRt2):F2} µs/op)");
|
||||
}
|
||||
|
||||
var memPackAvgSize2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray)).Average(r => r.SerializedSize);
|
||||
|
|
@ -1818,7 +1868,7 @@ public static class Program
|
|||
sb.AppendLine();
|
||||
sb.AppendLine("## Results");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("TestData | Engine | IO | Mode | Options | Size(B) | Ser(ms) | Deser(ms) | RT(ms) | SerAlloc(B/op) | DesAlloc(B/op) | RTAlloc(B/op) | SetupAlloc(B)");
|
||||
sb.AppendLine("TestData | Engine | IO | Mode | Options | Size(B) | Ser(µs/op) | Deser(µs/op) | RT(µs/op) | SerAlloc(KB/op) | DesAlloc(KB/op) | RTAlloc(KB/op) | Setup S/D(KB)");
|
||||
sb.AppendLine("---|---|---|---|---|---|---|---|---|---|---|---|---");
|
||||
|
||||
foreach (var testData in testDataSets)
|
||||
|
|
@ -1831,13 +1881,13 @@ public static class Program
|
|||
foreach (var r in testResults)
|
||||
{
|
||||
var inv = System.Globalization.CultureInfo.InvariantCulture;
|
||||
var ser = r.SerializeTimeMs > 0 ? r.SerializeTimeMs.ToString("F2", inv) : "-";
|
||||
var des = r.DeserializeTimeMs > 0 ? r.DeserializeTimeMs.ToString("F2", inv) : "-";
|
||||
var rt = r.RoundTripTimeMs > 0 ? r.RoundTripTimeMs.ToString("F2", inv) : "-";
|
||||
var serAlloc = r.SerializeTimeMs > 0 ? r.SerializeAllocBytesPerOp.ToString(inv) : "-";
|
||||
var desAlloc = r.DeserializeTimeMs > 0 ? r.DeserializeAllocBytesPerOp.ToString(inv) : "-";
|
||||
var rtAlloc = r.RoundTripAllocBytesPerOp > 0 ? r.RoundTripAllocBytesPerOp.ToString(inv) : "-";
|
||||
var setupAlloc = r.SetupAllocBytes.ToString(inv);
|
||||
var ser = r.SerializeTimeMs > 0 ? ToPerOpMicros(r.SerializeTimeMs).ToString("F2", inv) : "-";
|
||||
var des = r.DeserializeTimeMs > 0 ? ToPerOpMicros(r.DeserializeTimeMs).ToString("F2", inv) : "-";
|
||||
var rt = r.RoundTripTimeMs > 0 ? ToPerOpMicros(r.RoundTripTimeMs).ToString("F2", inv) : "-";
|
||||
var serAlloc = r.SerializeTimeMs > 0 ? ToKilobytes(r.SerializeAllocBytesPerOp).ToString("F2", inv) : "-";
|
||||
var desAlloc = r.DeserializeTimeMs > 0 ? ToKilobytes(r.DeserializeAllocBytesPerOp).ToString("F2", inv) : "-";
|
||||
var rtAlloc = r.RoundTripAllocBytesPerOp > 0 ? ToKilobytes(r.RoundTripAllocBytesPerOp).ToString("F2", inv) : "-";
|
||||
var setupAlloc = $"{ToKilobytes(r.SetupSerializeAllocBytes).ToString("F2", inv)} / {ToKilobytes(r.SetupDeserializeAllocBytes).ToString("F2", inv)}";
|
||||
sb.AppendLine($"{r.TestDataName} | {r.Engine} | {r.IoMode} | {r.DispatchMode} | {r.OptionsPreset} | {r.SerializedSize} | {ser} | {des} | {rt} | {serAlloc} | {desAlloc} | {rtAlloc} | {setupAlloc}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Server Logging
|
||||
|
||||
Server-side logging extensions. For core framework (base classes, configuration, LogLevel, ILogger bridge) see `AyCode.Core/AyCode.Core/docs/LOGGING/README.md`. For remote writers (HTTP, browser, SignalR) see `AyCode.Services/docs/LOGGING/README.md`.
|
||||
Server-side logging extensions. Core framework (base classes, config, LogLevel, ILogger bridge): `AyCode.Core/AyCode.Core/docs/LOGGING/README.md` | Remote writers (HTTP, browser, SignalR): `AyCode.Services/docs/LOGGING/README.md`.
|
||||
|
||||
## GlobalLogger
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# AyCode.Core.Server documentation
|
||||
|
||||
Topic documentation for the `AyCode.Core.Server` project (Layer 0, server-side).
|
||||
Topic docs for the `AyCode.Core.Server` project (Layer 0, server-side).
|
||||
|
||||
## Topics
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ Topic documentation for the `AyCode.Core.Server` project (Layer 0, server-side).
|
|||
|
||||
## Navigation
|
||||
|
||||
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Each topic folder has its own `README.md` with the main content, plus optional `TOPIC_ISSUES.md` (known issues) and `TOPIC_TODO.md` (planned work).
|
||||
Per the folder-navigation rule, start here when browsing `docs/`. Each topic folder has its own `README.md` (main content) + optional `TOPIC_ISSUES.md` and `TOPIC_TODO.md`.
|
||||
|
||||
## See also
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public enum TestUserRole
|
|||
/// Implements IId<int> for semantic $id/$ref serialization.
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class SharedTag : IId<int>
|
||||
{
|
||||
|
|
@ -80,7 +80,7 @@ public partial class SharedTag : IId<int>
|
|||
/// Shared category - for hierarchical cross-reference testing.
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class SharedCategory : IId<int>
|
||||
{
|
||||
|
|
@ -106,7 +106,7 @@ public partial class SharedCategory : IId<int>
|
|||
/// Shared user reference - appears in many places to test $ref deduplication.
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class SharedUser : IId<int>
|
||||
{
|
||||
|
|
@ -136,7 +136,7 @@ public partial class SharedUser : IId<int>
|
|||
/// User preferences - non-IId nested object
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class UserPreferences
|
||||
{
|
||||
|
|
@ -162,7 +162,7 @@ public partial class UserPreferences
|
|||
/// Does NOT implement IId, so uses standard Newtonsoft reference tracking.
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class MetadataInfo
|
||||
{
|
||||
|
|
@ -190,7 +190,7 @@ public partial class MetadataInfo
|
|||
/// Level 1: Main order - root of the hierarchy
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestOrder : IId<int>
|
||||
{
|
||||
|
|
@ -249,7 +249,7 @@ public partial class TestOrder : IId<int>
|
|||
/// <summary>
|
||||
/// Level 1: Main order - root of the hierarchy
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public partial class TestOrder_Circ_Ref : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -284,7 +284,7 @@ public partial class TestOrder_Circ_Ref : IId<int>
|
|||
/// Level 2: Order item with pallets
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestOrderItem : IId<int>
|
||||
{
|
||||
|
|
@ -323,7 +323,7 @@ public partial class TestOrderItem : IId<int>
|
|||
/// <summary>
|
||||
/// Level 2: Order item with pallets
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public partial class TestOrderItem_Circ_Ref : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -347,7 +347,7 @@ public partial class TestOrderItem_Circ_Ref : IId<int>
|
|||
/// Level 3: Pallet containing measurements
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestPallet : IId<int>
|
||||
{
|
||||
|
|
@ -390,7 +390,7 @@ public partial class TestPallet : IId<int>
|
|||
/// Level 4: Measurement with multiple points
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestMeasurement : IId<int>
|
||||
{
|
||||
|
|
@ -425,7 +425,7 @@ public partial class TestMeasurement : IId<int>
|
|||
/// Level 5: Deepest level - measurement point
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestMeasurementPoint : IId<int>
|
||||
{
|
||||
|
|
@ -459,7 +459,7 @@ public partial class TestMeasurementPoint : IId<int>
|
|||
/// <summary>
|
||||
/// Order with Guid Id - for testing Guid-based IId
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class TestGuidOrder : IId<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
|
@ -471,7 +471,7 @@ public class TestGuidOrder : IId<Guid>
|
|||
/// <summary>
|
||||
/// Item with Guid Id
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class TestGuidItem : IId<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
|
@ -487,7 +487,7 @@ public class TestGuidItem : IId<Guid>
|
|||
/// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values
|
||||
/// are stored as strings in the database.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class TestGenericAttribute
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -499,7 +499,7 @@ public class TestGenericAttribute
|
|||
/// DTO with GenericAttributes collection - simulates OrderDto with string-stored DateTime values.
|
||||
/// This reproduces the production bug where Binary serialization was thought to corrupt DateTime strings.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class TestDtoWithGenericAttributes : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -510,7 +510,7 @@ public class TestDtoWithGenericAttributes : IId<int>
|
|||
/// <summary>
|
||||
/// Order with nullable collections for null vs empty testing
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class TestOrderWithNullableCollections
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -523,7 +523,7 @@ public class TestOrderWithNullableCollections
|
|||
/// Class with all primitive types for WASM/serialization testing
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public partial class PrimitiveTestClass
|
||||
{
|
||||
public int IntValue { get; set; }
|
||||
|
|
@ -546,7 +546,7 @@ public partial class PrimitiveTestClass
|
|||
/// Class with extended primitive types for full serializer coverage.
|
||||
/// Includes DateTimeOffset, TimeSpan, Dictionary, null properties.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ExtendedPrimitiveTestClass
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -576,7 +576,7 @@ public class ExtendedPrimitiveTestClass
|
|||
/// <summary>
|
||||
/// Class with array of objects containing null items for WriteNull coverage
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ObjectWithNullItems
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -591,7 +591,7 @@ public class ObjectWithNullItems
|
|||
/// "Server-side" DTO with extra properties that the "client" doesn't know about.
|
||||
/// Used to test SkipValue functionality when deserializing unknown properties.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ServerCustomerDto : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -624,7 +624,7 @@ public class ServerCustomerDto : IId<int>
|
|||
/// the deserializer must skip unknown properties correctly
|
||||
/// while still maintaining string intern table consistency.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ClientCustomerDto : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -638,7 +638,7 @@ public class ClientCustomerDto : IId<int>
|
|||
/// Server DTO with nested objects that client doesn't know about.
|
||||
/// Tests skipping complex nested structures.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ServerOrderWithExtras : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -659,7 +659,7 @@ public class ServerOrderWithExtras : IId<int>
|
|||
/// <summary>
|
||||
/// Client version of the order - doesn't have Customer/RelatedCustomers properties.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(false, true, false, false)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ClientOrderSimple : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Serializers
|
||||
|
||||
High-performance serialization framework supporting three formats — Binary, JSON, and Toon — built on a shared infrastructure.
|
||||
High-performance serialization framework — Binary, JSON, Toon — on shared infrastructure.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ High-performance serialization framework supporting three formats — Binary, JS
|
|||
### Utilities
|
||||
|
||||
- **`AcSerializerCommon.cs`** — Expression type checking, queryable type refs, `ThreadLocal` cache helpers, compiled constructor creation.
|
||||
- **`IdentityMap.cs`** — High-performance hash table optimized for small int keys (bitmap) + chaining for large keys. Used for reference tracking.
|
||||
- **`IdentityMap.cs`** — High-perf hash table optimized for small int keys (bitmap) + chaining for large keys. Used for reference tracking.
|
||||
- **`FnvHash.cs`** — Deterministic FNV-1a property name hashing (used in Binary `UseMetadata` mode).
|
||||
- **`ReferenceTracker.cs`** — `ReferenceEqualityComparer` for object identity-based tracking dictionaries.
|
||||
- **`IIdCollectionMergeHelper.cs`** — Collection merge operations during `PopulateMerge` (handles orphaned item removal).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# AcBinary Features
|
||||
|
||||
Advanced serialization features built on top of the wire format. For core type markers and encoding see `BINARY_FORMAT.md`. For configuration options and presets see `BINARY_OPTIONS.md`. For internal architecture and memory management see `BINARY_IMPLEMENTATION.md`. For source generation details see `BINARY_SGEN.md`.
|
||||
Advanced serialization features on top of the wire format. Wire format: `BINARY_FORMAT.md` | Options/presets: `BINARY_OPTIONS.md` | Internal architecture: `BINARY_IMPLEMENTATION.md` | Source generation: `BINARY_SGEN.md`.
|
||||
|
||||
## Compact Encoding Selection
|
||||
|
||||
|
|
@ -95,8 +95,8 @@ This order is stable across serializer/deserializer as long as the type hierarch
|
|||
When `UseMetadata=true`, property name hashes (FNV-1a via `FnvHash.ComputeString`) are written per type, enabling schema evolution:
|
||||
|
||||
- **Serializer** writes property hashes in the metadata section (`ObjectWithMetadata(69)`)
|
||||
- **Deserializer** builds an index mapping array (`GetIndexMapping()`) that maps source property indices to destination indices by matching FNV-1a hashes
|
||||
- This allows deserialization even when source and destination types have different property sets or ordering
|
||||
- **Deserializer** builds an index mapping array (`GetIndexMapping()`) — maps source property indices to destination via FNV-1a hash matching
|
||||
- Allows deserialization across different property sets or ordering
|
||||
|
||||
When `UseMetadata=false`, properties are matched by **positional index only** — source and destination must have identical property layouts.
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ Single-byte object type. The marker byte **is** the type slot index — no addit
|
|||
[FixObj(N)] [properties...]
|
||||
```
|
||||
|
||||
**Slot allocation:** Slots 0–63 are reserved for runtime polymorphic types, assigned dynamically on first encounter during serialization. Source-generated (SGen) types receive slots starting at 64+ via `AllocateWrapperSlot()` (sequential, `Interlocked.Increment`). SGen slots are compile-time stable; runtime slots depend on serialization order.
|
||||
**Slot allocation:** Slots 0–63 reserved for runtime polymorphic types, assigned dynamically on first encounter. Source-generated (SGen) types receive slots from 64+ via `AllocateWrapperSlot()` (sequential, `Interlocked.Increment`). SGen slots are compile-time stable; runtime slots depend on serialization order.
|
||||
|
||||
### Complex Types (64–71)
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
# AcBinary Configuration
|
||||
|
||||
Configuration options, presets, and option interactions for `AcBinarySerializerOptions`. For wire format see `BINARY_FORMAT.md`. For features (interning, ref tracking, property ordering) see `BINARY_FEATURES.md`. For internal architecture and memory management see `BINARY_IMPLEMENTATION.md`.
|
||||
Configuration options, presets, and option interactions for `AcBinarySerializerOptions`. Wire format: `BINARY_FORMAT.md` | Features: `BINARY_FEATURES.md` | Internal architecture: `BINARY_IMPLEMENTATION.md`.
|
||||
|
||||
## WireMode
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ Configuration options, presets, and option interactions for `AcBinarySerializerO
|
|||
| `OnlyId` | `IId` objects only (by ID value) | Partial | `0x02` | `ObjectRefFirst(70)` + `ObjectRef(65)` |
|
||||
| `All` (default) | All reference types | Full graph walk | `0x06` | `ObjectRefFirst(70)` + `ObjectRef(65)` |
|
||||
|
||||
**Format impact:** When enabled, multi-referenced objects are written once with `ObjectRefFirst(70) + VarUInt(refCacheIndex)` on first encounter, then replaced by `ObjectRef(65) + VarUInt(refCacheIndex)` on subsequent encounters. Header `HasCacheCount` flag is set and cache count written.
|
||||
**Format impact:** Multi-referenced objects written once with `ObjectRefFirst(70) + VarUInt(refCacheIndex)` on first encounter, then `ObjectRef(65) + VarUInt(refCacheIndex)` subsequently. Header `HasCacheCount` flag set; cache count written.
|
||||
|
||||
**Interaction with `ThrowOnCircularReference` (default: `true`):**
|
||||
- `true` + ref handling enabled: all objects tracked for cycle detection, throws `InvalidOperationException` on circular reference
|
||||
|
|
@ -40,7 +40,7 @@ Configuration options, presets, and option interactions for `AcBinarySerializerO
|
|||
|
||||
**Code branch:** `context.UseMetadata` controls whether `ObjectWithMetadata(69)` or plain `Object(64)` markers are used. When `false`, `IsDirectObjectWrite=true` allows source-generated writers to bypass `WriteObject` entirely and inline property writes.
|
||||
|
||||
**Related:** `CheckDuplicatePropName` (default: `true`) — throws if FNV-1a hash collision detected between property names of the same type. ⚠️ Disabling for performance trades correctness for speed — see [`BINARY_ISSUES.md#accore-bin-i-c5r7`](BINARY_ISSUES.md#accore-bin-i-c5r7-checkduplicatepropnamefalse-silently-corrupts-on-fnv-1a-hash-collision) before turning off.
|
||||
**Related:** `CheckDuplicatePropName` (default `true`) — throws on FNV-1a hash collision between property names of the same type. ⚠️ Disabling trades correctness for speed — see [`BINARY_ISSUES.md#accore-bin-i-c5r7`](BINARY_ISSUES.md#accore-bin-i-c5r7-checkduplicatepropnamefalse-silently-corrupts-on-fnv-1a-hash-collision) before turning off.
|
||||
|
||||
## UseStringInterning
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ Configuration options, presets, and option interactions for `AcBinarySerializerO
|
|||
| `Attribute` (default) | Properties with `[AcStringIntern(true)]` | Scans marked properties | `StringInternFirst(94)` + `StringInterned(92)` |
|
||||
| `All` | All strings within length limits | Scans all strings | `StringInternFirst(94)` + `StringInterned(92)` |
|
||||
|
||||
**Length limits:** `MinStringInternLength` (default: 4) and `MaxStringInternLength` (default: 64, 0=unlimited). Strings outside this range are always written inline.
|
||||
**Length limits:** `MinStringInternLength` (default 4), `MaxStringInternLength` (default 64, 0=unlimited). Strings outside this range are always written inline.
|
||||
|
||||
**Format impact:** Interned strings on first occurrence: `[StringInternFirst(94)] [VarUInt cacheIndex] [string data]`. Subsequent: `[StringInterned(92)] [VarUInt cacheIndex]` (1–2 bytes vs full string). Single-occurrence strings are never interned — no overhead for unique strings.
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ Configuration options, presets, and option interactions for `AcBinarySerializerO
|
|||
|
||||
**Format impact:** Compression is applied **post-serialization** as a transparent wrapper — the inner wire format is unchanged. Both modes are pure managed C# (WASM-compatible, no native dependencies).
|
||||
|
||||
**Code branch:** Applied in `AcBinarySerializer.Serialize()` after the serialization context produces the raw buffer: `if (UseCompression != None) Lz4.Compress(buffer, mode)`. Decompression is automatic on deserialize.
|
||||
**Code branch:** Applied in `AcBinarySerializer.Serialize()` after the context produces the raw buffer: `if (UseCompression != None) Lz4.Compress(buffer, mode)`. Decompression auto on deserialize.
|
||||
|
||||
## PropertyFilter
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
|
|||
|
||||
## PropertyMapper
|
||||
|
||||
Optional delegate `PropertyMapperDelegate?` (default: `null`) for cross-type deserialization property remapping.
|
||||
Optional delegate `PropertyMapperDelegate?` (default `null`) for cross-type deserialization property remapping.
|
||||
|
||||
```
|
||||
delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type destinationType);
|
||||
|
|
@ -142,7 +142,7 @@ delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type
|
|||
|
||||
## Option Interactions
|
||||
|
||||
Key interdependencies that affect which code branches execute:
|
||||
Key interdependencies affecting code branches:
|
||||
|
||||
| Combination | Effect |
|
||||
|-------------|--------|
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -7,7 +7,7 @@ Custom logging framework with multi-writer fan-out and `Microsoft.Extensions.Log
|
|||
|
||||
## Design Overview
|
||||
|
||||
A logger holds a list of writers. Every log call fans out to all writers that pass the level filter. Two independent level gates control what gets written:
|
||||
Logger holds a list of writers. Every log call fans out, filtered by two independent level gates:
|
||||
|
||||
```
|
||||
logger.Info("msg")
|
||||
|
|
@ -69,13 +69,13 @@ public enum LogLevel : byte
|
|||
}
|
||||
```
|
||||
|
||||
⚠️ **Values are synchronized with the database `LogLevel` table.** Do NOT renumber.
|
||||
⚠️ **Values DB-synchronized with `LogLevel` table.** Do NOT renumber.
|
||||
|
||||
Comparison is `<=`: a logger/writer with `LogLevel = Info` will process `Info`, `Suggest`, `Warning`, `Error` (anything ≥ 15).
|
||||
Comparison `<=`: a logger/writer with `LogLevel = Info` processes `Info`, `Suggest`, `Warning`, `Error` (≥ 15).
|
||||
|
||||
## Configuration
|
||||
|
||||
All config lives under `AyCode:Logger` in `appsettings.json`:
|
||||
Config under `AyCode:Logger` in `appsettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -106,40 +106,32 @@ All config lives under `AyCode:Logger` in `appsettings.json`:
|
|||
|
||||
### Writer Instantiation
|
||||
|
||||
`AcLoggerBase` constructor iterates `LogWriters[]` and calls:
|
||||
`AcLoggerBase` constructor iterates `LogWriters[]` and calls `Activator.CreateInstance(logWriterType, AppType, logWriterLogLevel, CategoryName)` — each writer's ctor must accept `(AppType, LogLevel, string?)`.
|
||||
|
||||
```csharp
|
||||
Activator.CreateInstance(logWriterType, AppType, logWriterLogLevel, CategoryName)
|
||||
```
|
||||
|
||||
Each writer's constructor signature must accept `(AppType, LogLevel, string?)`.
|
||||
|
||||
### Writer Config Self-Lookup
|
||||
|
||||
`AcLogWriterBase` also reads its own `LogLevel` from config by matching its `AssemblyQualifiedName` against the `LogWriterType` entries. This means a writer instantiated manually (not via config) can still pick up config values if a matching entry exists.
|
||||
`AcLogWriterBase` also reads its own `LogLevel` from config by matching `AssemblyQualifiedName` against `LogWriterType` entries — a manually-instantiated writer (not via config) still picks up config values if a matching entry exists.
|
||||
|
||||
## DI-Based Factory Pattern
|
||||
|
||||
Modern, framework-first alternative to the config-reading `AcLoggerBase(string)` ctor. No runtime reflection over writer config; concrete writer types resolved from DI. Recommended for all modern projects (MAUI, WASM, ASP.NET Core) — the config-reading path is filesystem-bound and unsuitable for MAUI/WASM (see `LOGGING_ISSUES.md#accore-log-i-r9p3`).
|
||||
Modern alternative to the config-reading `AcLoggerBase(string)` ctor. Concrete writer types resolved from DI; no runtime reflection over writer config. Recommended for MAUI, WASM, ASP.NET Core — the config-reading path is filesystem-bound and unsuitable for MAUI/WASM (see `LOGGING_ISSUES.md#accore-log-i-r9p3`).
|
||||
|
||||
### Consumer setup in Program.cs
|
||||
|
||||
```csharp
|
||||
// 1. Bind options from appsettings.json
|
||||
// 1. Bind options
|
||||
services.Configure<AcLoggerOptions>(configuration.GetSection("AyCode:Logger"));
|
||||
|
||||
// 2. Register writer(s) as DI singletons
|
||||
services.AddSingleton<IAcLogWriterBase, MyConsoleWriter>();
|
||||
// Or, for client-scoped marker:
|
||||
// Client-scoped marker variant:
|
||||
// services.AddSingleton<IAcLogWriterClientBase, MyRemoteWriter>();
|
||||
|
||||
// 3. Register the logger factory — registers Func<string, TLogger> singleton in DI
|
||||
// 3. Register the logger factory (Func<string, TLogger> singleton in DI)
|
||||
services.AddAcLoggerFactory<MyConcreteLogger>();
|
||||
// Or, with custom writer marker (pulls only TWriterBase-registered writers):
|
||||
// Custom writer-marker overload — pulls only TWriterBase-registered writers:
|
||||
// services.AddAcLoggerFactory<MyConcreteLogger, IAcLogWriterClientBase>();
|
||||
```
|
||||
|
||||
Consumers inject `Func<string, MyConcreteLogger>` and invoke it with a category name to obtain category-scoped logger instances.
|
||||
Consumers inject `Func<string, MyConcreteLogger>` and invoke it with a category name.
|
||||
|
||||
### Appsettings.json shape
|
||||
|
||||
|
|
@ -154,39 +146,23 @@ Consumers inject `Func<string, MyConcreteLogger>` and invoke it with a category
|
|||
}
|
||||
```
|
||||
|
||||
Only `AppType` and `LogLevel` bind to `AcLoggerOptions`. The legacy `LogWriters[]` array used by the config-reading ctor is independent and unused in this pattern.
|
||||
|
||||
### AcLoggerOptions
|
||||
|
||||
| Property | Type | Default | Purpose |
|
||||
|----------|------|---------|---------|
|
||||
| `AppType` | `AppType` | `Server` | Stamped on each log entry |
|
||||
| `LogLevel` | `LogLevel` | `Info` | Global minimum level (Logger's own gate) |
|
||||
|
||||
Extend the POCO as needed; consumer's `services.Configure<AcLoggerOptions>` binding picks up new properties automatically.
|
||||
Only `AppType` and `LogLevel` bind to `AcLoggerOptions` (defaults: `Server` / `Info`). Extend the POCO as needed; the binding picks up new properties automatically. The legacy `LogWriters[]` array (config-reading ctor) is unused in this pattern.
|
||||
|
||||
### TLogger ctor requirement
|
||||
|
||||
`AddAcLoggerFactory<TLogger>` uses `Activator.CreateInstance(typeof(TLogger), AppType, LogLevel, categoryName, writers)`. `TLogger` must expose a public constructor with signature:
|
||||
`AddAcLoggerFactory<TLogger>` uses `Activator.CreateInstance(typeof(TLogger), AppType, LogLevel, categoryName, writers)` — `TLogger` must expose:
|
||||
```csharp
|
||||
public TLogger(AppType appType, LogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters)
|
||||
```
|
||||
|
||||
If your concrete logger doesn't have this signature, register a manual factory instead:
|
||||
If the concrete logger doesn't match, register a manual factory instead:
|
||||
```csharp
|
||||
services.AddSingleton<Func<string, MyConcreteLogger>>(sp => category => new MyConcreteLogger(...));
|
||||
```
|
||||
|
||||
### Writer-marker scoping (two overloads)
|
||||
|
||||
- `AddAcLoggerFactory<TLogger>()` — all `IAcLogWriterBase`-registered writers go into the logger
|
||||
- `AddAcLoggerFactory<TLogger, TWriterBase>()` — only writers registered as `TWriterBase` (e.g. `IAcLogWriterClientBase` for client-only)
|
||||
|
||||
Use the two-arg overload when the consumer separates client-only and server-only writers via a marker interface.
|
||||
|
||||
### Companion extension: AddAcDefaults (SignalR)
|
||||
|
||||
For SignalR client setup that wires the same `AcLoggerBase` instance into Microsoft.Extensions.Logging, use `AcSignalRConnectionExtensions.AddAcDefaults(builder, logger, connectionOptions)` — it bundles `AddAcConnection` + `ConfigureLogging(AddAcLogger)` in a single call. See `AyCode.Services/docs/SIGNALR/README.md`.
|
||||
For SignalR client setup, `AcSignalRConnectionExtensions.AddAcDefaults(builder, logger, connectionOptions)` bundles `AddAcConnection` + `ConfigureLogging(AddAcLogger)` in one call. See `AyCode.Services/docs/SIGNALR/README.md`.
|
||||
|
||||
### Comparison: config-reading ctor vs DI-based factory
|
||||
|
||||
|
|
@ -224,9 +200,9 @@ Abstract logger implementing both `IAcLogWriterBase` and `ILogger`. Central resp
|
|||
|
||||
### AcLogWriterBase
|
||||
|
||||
Abstract writer base. Each writer has its own `LogLevel` and `AppType`. Named methods (`Detail`, `Debug`, etc.) delegate to the terminal `Write(AppType, LogLevel, text, caller, category, errorType, exMessage)` which subclasses override.
|
||||
Abstract writer base. Each writer has own `LogLevel` and `AppType`. Named methods (`Detail`, `Debug`, etc.) delegate to terminal `Write(AppType, LogLevel, text, caller, category, errorType, exMessage)`.
|
||||
|
||||
The base `Write` throws `NotImplementedException` — subclasses **must** override either the 7-parameter `Write` (for text writers) or `Write(IAcLogItemClient)` (for structured writers), or both.
|
||||
Base `Write` throws `NotImplementedException` — subclasses **must** override either 7-param `Write` (text writers) or `Write(IAcLogItemClient)` (structured), or both.
|
||||
|
||||
### AcTextLogWriterBase
|
||||
|
||||
|
|
@ -260,7 +236,7 @@ Colored console output. Thread-safe via `static readonly object ForWriterLock`.
|
|||
|
||||
### ILogger Bridge
|
||||
|
||||
`AcLoggerBase` implements `ILogger` directly. The `Log<TState>` method maps MS log levels to AC methods:
|
||||
`Log<TState>` maps MS log levels to AC methods:
|
||||
|
||||
| Microsoft.Extensions.Logging.LogLevel | AyCode LogLevel | Method called |
|
||||
|---------------------------------------|-----------------|---------------|
|
||||
|
|
@ -272,15 +248,11 @@ Colored console output. Thread-safe via `static readonly object ForWriterLock`.
|
|||
| Critical | Error | `Error("[CRITICAL] ...")` |
|
||||
| None | Disabled | — (ignored) |
|
||||
|
||||
`BeginScope` returns a no-op `NullScope` (scopes not supported).
|
||||
|
||||
**ShortenCategoryNames** (default: `true`): When MS logging provides fully-qualified type names as categories (e.g., `Microsoft.AspNetCore.SignalR.HubConnectionHandler`), shortens to just the class name (`HubConnectionHandler`).
|
||||
`BeginScope` → no-op `NullScope` (scopes not supported). **ShortenCategoryNames** (default `true`): when MS logging passes fully-qualified type names (e.g. `Microsoft.AspNetCore.SignalR.HubConnectionHandler`), shortens to class name only (`HubConnectionHandler`).
|
||||
|
||||
### ILoggerProvider
|
||||
|
||||
`AcLoggerProvider<TLogger>` implements `ILoggerProvider` with a `ConcurrentDictionary<string, TLogger>` per-category cache. Factory function provided at registration.
|
||||
|
||||
**Extension methods:**
|
||||
`AcLoggerProvider<TLogger>` implements `ILoggerProvider` with a per-category `ConcurrentDictionary<string, TLogger>` cache. Factory function provided at registration.
|
||||
|
||||
```csharp
|
||||
// Add alongside default providers
|
||||
|
|
@ -306,11 +278,11 @@ IAcLogItemClient (AyCode.Core — DTO interface)
|
|||
|
||||
### [Conditional("DEBUG")] Pattern
|
||||
|
||||
Every named log method has a `*Conditional` counterpart (e.g., `InfoConditional`, `ErrorConditional`) decorated with `[Conditional("DEBUG")]`. These are completely stripped from Release builds by the compiler. Both `AcLoggerBase` and `AcLogWriterBase` provide these. Use them for development-only diagnostics that should have zero cost in production.
|
||||
Every named log method has a `*Conditional` counterpart (`InfoConditional`, `ErrorConditional`, ...) decorated `[Conditional("DEBUG")]` — stripped from Release builds. Provided by both `AcLoggerBase` and `AcLogWriterBase`. Use for dev-only diagnostics with zero release cost.
|
||||
|
||||
### CallerMemberName Auto-Capture
|
||||
|
||||
All named log methods use `[CallerMemberName] string? memberName = null`. The compiler auto-fills the calling method name. For `ILogger.Log<TState>` calls (from MS logging), the `EventId.Name` is used as member name if available, otherwise defaults to `"Log"`.
|
||||
Named log methods use `[CallerMemberName] string? memberName = null` — compiler auto-fills caller name. `ILogger.Log<TState>` (MS logging) uses `EventId.Name` if available, else `"Log"`.
|
||||
|
||||
## Key Source Files
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# AyCode.Core documentation
|
||||
|
||||
Topic documentation for the `AyCode.Core` project (Layer 0 framework).
|
||||
Topic docs for the `AyCode.Core` project (Layer 0 framework).
|
||||
|
||||
## Topics
|
||||
|
||||
|
|
@ -11,4 +11,4 @@ Topic documentation for the `AyCode.Core` project (Layer 0 framework).
|
|||
|
||||
## Navigation
|
||||
|
||||
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Each topic folder has its own `README.md` with the main content, plus optional `ISSUES.md` (known issues) and `TODO.md` (planned work).
|
||||
Per the folder-navigation rule, start here when browsing `docs/`. Each topic folder has its own `README.md` (main content) + optional `ISSUES.md` (known issues) and `TODO.md` (planned work).
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
# TOON — Token-Oriented Object Notation
|
||||
|
||||
LLM-optimized serializer. Primary goal: **maximize LLM accuracy** via explicit schema/data separation, rich metadata, and unambiguous structure boundaries. Serialize-only (no deserializer — see `TOON_TODO.md#accore-toon-t-f3x1`).
|
||||
LLM-optimized serializer. Primary goal: **maximize LLM accuracy** via schema/data separation, rich metadata, unambiguous boundaries. Serialize-only (no deserializer — see `TOON_TODO.md#accore-toon-t-f3x1`).
|
||||
|
||||
Source: `Serializers/Toons/` in this project.
|
||||
|
||||
## Design goals
|
||||
|
||||
1. **Zero ambiguity** — explicit `{}` / `[]` / `"""` boundaries; no indentation-only scoping.
|
||||
2. **Rich semantic context** — every property carries description, purpose, constraints, examples in the schema section.
|
||||
3. **Token efficiency** — schema sent once (`@meta`/`@types`), data streamed thereafter (`@data`); enum values numeric rather than by name.
|
||||
4. **Smart defaults** — useful output without any attributes; `ToonDescriptionAttribute` for precision when conventions fall short.
|
||||
2. **Rich semantic context** — every property carries description, purpose, constraints, examples in schema.
|
||||
3. **Token efficiency** — schema once (`@meta`/`@types`), data streamed (`@data`); enum values numeric.
|
||||
4. **Smart defaults** — useful output without attributes; `ToonDescriptionAttribute` for precision where conventions fall short.
|
||||
|
||||
## Three-section output
|
||||
|
||||
|
|
@ -23,9 +23,9 @@ Source: `Serializers/Toons/` in this project.
|
|||
|
||||
| Mode | When to use |
|
||||
|---|---|
|
||||
| `Full` (default) | First serialization — LLM needs the full schema + data context. |
|
||||
| `MetaOnly` | Send schema once at conversation start; subsequent sends can skip it. |
|
||||
| `DataOnly` | Subsequent sends when the LLM already has the schema — 30-50% token savings. |
|
||||
| `Full` (default) | First serialization — LLM needs full schema + data. |
|
||||
| `MetaOnly` | Send schema once at conversation start. |
|
||||
| `DataOnly` | Subsequent sends; LLM has the schema already — 30-50% token savings. |
|
||||
|
||||
Details, preset breakdown, all options: `TOON_OPTIONS.md`.
|
||||
|
||||
|
|
@ -38,36 +38,18 @@ var person = new Person { Id = 1, Name = "Alice", Email = "alice@example.com" };
|
|||
string toon = AcToonSerializer.Serialize(person);
|
||||
```
|
||||
|
||||
Output (Full mode, abbreviated):
|
||||
Output (Full mode, abbreviated — full grammar in `TOON_FORMAT.md`):
|
||||
|
||||
```toon
|
||||
@meta {
|
||||
version = "1.0"
|
||||
format = "toon"
|
||||
source-code-language = "C#"
|
||||
types = ["Person"]
|
||||
}
|
||||
|
||||
@meta { version = "1.0", format = "toon", types = ["Person"] }
|
||||
@types {
|
||||
Person: "Object of type Person"
|
||||
Id: int32
|
||||
description: "Unique identifier for Person"
|
||||
purpose: "Primary key / unique identification"
|
||||
constraints: "required"
|
||||
Name: string
|
||||
description: "Name of the Person"
|
||||
constraints: "required"
|
||||
Email: string
|
||||
description: "Email address"
|
||||
constraints: "required, email-format"
|
||||
Person:
|
||||
Id: int32 (description, purpose, constraints: "required")
|
||||
Name: string (description, constraints: "required")
|
||||
Email: string (description, constraints: "required, email-format")
|
||||
}
|
||||
|
||||
@data {
|
||||
Person {
|
||||
Id = 1
|
||||
Name = "Alice"
|
||||
Email = "alice@example.com"
|
||||
}
|
||||
Person { Id = 1, Name = "Alice", Email = "alice@example.com" }
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ The `@types` section body (when emitted) follows the same topological order.
|
|||
- Optional lines emitted only when the value is non-empty.
|
||||
- `navigation` values are kebab-case of `ToonRelationType`: `many-to-one`, `one-to-many`, `one-to-one`, `many-to-many`.
|
||||
- `typeHint` uses **C# short names** — primitives: `int`, `long`, `short`, `byte`, `sbyte`, `uint`, `ulong`, `ushort`, `double`, `float`, `decimal`, `bool`, `string`, `char`; special types: `DateTime`, `DateTimeOffset`, `TimeSpan`, `Guid`; collections: `Person[]`, `List<Person>`, `Dictionary<string, int>`; class / enum names as-is. Nullable suffix: `int?`, `DateTime?`, `TaxDisplayType?`.
|
||||
- ⚠️ **Two naming conventions coexist.** The `@types` section uses C# short names (above). The `@data` inline type hints (when `UseInlineTypeHints = true`) use full-width tokens like `int32`, `float64`, `datetime` — see the Inline type hints subsection below.
|
||||
- ⚠️ **Two naming conventions coexist.** `@types` uses C# short names (above); `@data` inline type hints (when `UseInlineTypeHints = true`) use full-width tokens like `int32`, `float64`, `datetime` — see Inline type hints below.
|
||||
|
||||
### Enum types in `@types`
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ Age = 30 <int32>
|
|||
Name = "Alice" <string>
|
||||
```
|
||||
|
||||
Hint tokens (full-width naming — **distinct** from the C# short names used in `@types`): `string`, `int32`, `int64`, `int16`, `byte`, `sbyte`, `uint16`, `uint32`, `uint64`, `bool`, `float32`, `float64`, `decimal`, `char`, `datetime`, `datetimeoffset`, `timespan`, `guid`, `enum`. Disabled by default (the same information lives in `@types`).
|
||||
Hint tokens (full-width naming — **distinct** from C# short names in `@types`): `string`, `int32`, `int64`, `int16`, `byte`, `sbyte`, `uint16`, `uint32`, `uint64`, `bool`, `float32`, `float64`, `decimal`, `char`, `datetime`, `datetimeoffset`, `timespan`, `guid`, `enum`. Disabled by default (info lives in `@types`).
|
||||
|
||||
## Compact mode quirks
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Internal design of `AcToonSerializer`. Read this when modifying the serializer.
|
|||
|
||||
## Partial class breakdown
|
||||
|
||||
`AcToonSerializer` is a `static partial class` split across topical files for cohesion. Each file contributes methods to the same class — no separate types, no inheritance between partials.
|
||||
`AcToonSerializer` is a `static partial class` split across topical files for cohesion. Each file contributes methods to the same class — no separate types, no inheritance.
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
|
|
@ -50,7 +50,7 @@ AcSerializerContextBase<TMetadata, TOptions> (Serializers/ root)
|
|||
- `CustomDescription` — the `ToonDescriptionAttribute` instance if present.
|
||||
- Flags: `IsCollection`, `IsDictionary`, `ElementType`.
|
||||
|
||||
Shared infrastructure (metadata cache semantics, `IdentityMap`, `ReferenceTracker`): see `../../Serializers/README.md`. Not duplicated here.
|
||||
Shared infrastructure (metadata cache, `IdentityMap`, `ReferenceTracker`): see `../../Serializers/README.md`.
|
||||
|
||||
## Serialization flow
|
||||
|
||||
|
|
|
|||
|
|
@ -50,12 +50,12 @@ Matching is case-insensitive where noted and uses `string.Contains` or `StartsWi
|
|||
|
||||
## Type-derived and name-based constraints
|
||||
|
||||
Constraints join with `, `. Inference is **redundancy-conscious** — Toon skips constraints already implied by the property's type hint (visible on the `PropertyName: typeHint` line) to save LLM tokens. Source of this design decision: inline code comments in `AcToonSerializer.AttributeExtraction.cs` → `ExtractTypeConstraints`.
|
||||
Constraints join with `, `. Inference is **redundancy-conscious** — Toon skips constraints already implied by the type hint (`PropertyName: typeHint`) to save LLM tokens. Source: inline comments in `AcToonSerializer.AttributeExtraction.cs` → `ExtractTypeConstraints`.
|
||||
|
||||
### Not emitted automatically
|
||||
|
||||
- **`nullable` / `required`** — the type hint conveys this (`int?` vs `int`, `DateTime?` vs `DateTime`, reference type vs non-nullable value type). `required` IS emitted when the property carries an explicit `[Required]` DataAnnotation — that signals intent beyond what the type says.
|
||||
- **Large-range numeric bounds** — no auto range on `int`, `long`, `ulong`, `float`, `double`, `decimal`. The type name already bounds the value; `range: -2147483648-2147483647` would be noise.
|
||||
- **`nullable` / `required`** — type hint conveys this (`int?` vs `int`, `DateTime?` vs `DateTime`, reference vs non-nullable value type). `required` IS emitted when an explicit `[Required]` DataAnnotation is present — signals intent beyond the type.
|
||||
- **Large-range numeric bounds** — no auto range on `int`, `long`, `ulong`, `float`, `double`, `decimal`. Type name already bounds the value; `range: -2147483648-2147483647` would be noise.
|
||||
- **Enum property constraints** — none. The `@types` enum definition holds the name ↔ numeric value map.
|
||||
|
||||
### Emitted automatically — small-integer ranges
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public enum ToonSerializationMode : byte
|
|||
|
||||
## Presets
|
||||
|
||||
Each preset is a fresh instance (static property with `new` expression — no shared state between retrievals).
|
||||
Each preset is a fresh instance (static property with `new` expression; no shared state).
|
||||
|
||||
### `Default`
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ OmitDefaultValues = true, WriteTypeNames = false,
|
|||
ReferenceHandling = None
|
||||
```
|
||||
|
||||
Data-only, no type names, no reference tracking. Note: `UseIndentation = true` here — truly token-minimal output requires manually setting `UseIndentation = false` on a custom options instance.
|
||||
Data-only, no type names, no reference tracking. Note: `UseIndentation = true` here — truly token-minimal output requires `UseIndentation = false` on a custom options instance.
|
||||
|
||||
### `Verbose`
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Per the `REPO_PREFIXES.md` and `TOPIC_CODES.md` registries:
|
|||
|
||||
## Historical note
|
||||
|
||||
Before this folder existed, cross-cutting entries were duplicated in each affected topic's file (e.g., `XCUT-1` appeared both in `BINARY_ISSUES.md` and `SIGNALR_ISSUES.md`). The canonical-home pattern here replaces that — one authoritative entry, short cross-refs from each affected topic.
|
||||
Before this folder existed, cross-cutting entries were duplicated in each affected topic's file (e.g., `XCUT-1` in both `BINARY_ISSUES.md` and `SIGNALR_ISSUES.md`). The canonical-home pattern replaces that — one authoritative entry, short cross-refs from each affected topic.
|
||||
|
||||
## See also
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# AyCode.Services.Server documentation
|
||||
|
||||
Topic documentation for the `AyCode.Services.Server` project (Layer 0, server-side services).
|
||||
Topic docs for the `AyCode.Services.Server` project (Layer 0, server-side services).
|
||||
|
||||
## Topics
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ Topic documentation for the `AyCode.Services.Server` project (Layer 0, server-si
|
|||
|
||||
## Navigation
|
||||
|
||||
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Each topic folder has its own `README.md` with the main content, plus optional `TOPIC_ISSUES.md` (known issues) and `TOPIC_TODO.md` (planned work).
|
||||
Per the folder-navigation rule, start here when browsing `docs/`. Each topic folder has its own `README.md` (main content) + optional `TOPIC_ISSUES.md` and `TOPIC_TODO.md`.
|
||||
|
||||
## See also
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ Reflection runs lazily per tag on first request, then results are cached statica
|
|||
ConcurrentDictionary<TSessionItemId, TSessionItem> Sessions
|
||||
```
|
||||
|
||||
`IAcSessionItem<TSessionItemId>` requires `SessionId` property. Used for targeting messages to specific users/connections.
|
||||
`IAcSessionItem<TSessionItemId>` requires `SessionId` property. Targets messages to specific users/connections.
|
||||
|
||||
## Broadcast Service
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Change-tracked real-time collection on top of the SignalR transport layer. Sourc
|
|||
|
||||
## Overview
|
||||
|
||||
`AcSignalRDataSource` is an **abstract** generic `IList<T>` that synchronizes with the server via CRUD tags. It handles change tracking, rollback, sync state, and pluggable Binary/JSON+GZip deserialization — so consuming code works with a regular list while the DataSource manages communication.
|
||||
`AcSignalRDataSource` — **abstract** generic `IList<T>` synchronized via CRUD tags. Handles change tracking, rollback, sync state, pluggable Binary/JSON+GZip deserialization — consuming code works with a regular list while the DataSource manages communication.
|
||||
|
||||
```csharp
|
||||
public abstract class AcSignalRDataSource<TDataItem, TId, TIList> : IList<TDataItem>, IList, IReadOnlyList<TDataItem>
|
||||
|
|
@ -38,7 +38,7 @@ new MySampleDataSource(
|
|||
|
||||
## SignalRCrudTags
|
||||
|
||||
A `sealed class` that bundles **5 independent tag integers** — one per CRUD operation. Tags are NOT sequential offsets; each tag is independently assigned:
|
||||
A `sealed class` bundling **5 independent tag integers** — one per CRUD operation. Tags are NOT sequential offsets; each is independently assigned:
|
||||
|
||||
```csharp
|
||||
public sealed class SignalRCrudTags(
|
||||
|
|
@ -70,7 +70,7 @@ var crudTags = new SignalRCrudTags(
|
|||
);
|
||||
```
|
||||
|
||||
**Tag lookup:** `GetMessageTagByTrackingState(TrackingState)` maps tracking state to the corresponding tag via switch expression.
|
||||
**Tag lookup:** `GetMessageTagByTrackingState(TrackingState)` — switch expression mapping state → tag.
|
||||
|
||||
## Serializer Selection
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ var crudTags = new SignalRCrudTags(
|
|||
public AcSerializerType SerializerType { get; set; } = AcSerializerType.Binary;
|
||||
```
|
||||
|
||||
Controls the deserialization format on the **raw byte[]** load path (see Data Loading). Must match the server's `SerializerOptions.SerializerType` for that endpoint. Default: `Binary`. Override to `JsonGZip` for JSON datasources.
|
||||
Controls deserialization format on the **raw byte[]** load path (see Data Loading). Must match server's `SerializerOptions.SerializerType` for that endpoint. Default `Binary`; override to `JsonGZip` for JSON datasources.
|
||||
|
||||
## Data Loading
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ RemoveAt(index, autoSave): → by index
|
|||
|
||||
`autoSave` parameter — if true, immediately calls `SaveItem()` for that single change after the local mutation.
|
||||
|
||||
**Manual tracking:** `SetTrackingStateToUpdate(item)` marks an existing item as modified without replacing it — useful when properties are mutated in-place.
|
||||
**Manual tracking:** `SetTrackingStateToUpdate(item)` marks existing item as modified without replacing — for in-place property mutations.
|
||||
|
||||
### Additional collection helpers
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ TIList innerList = dataSource.GetReferenceInnerList();
|
|||
bool hasRef = dataSource.HasWorkingReferenceList;
|
||||
```
|
||||
|
||||
Useful when the UI already has a bound collection and you want the DataSource to manage it in-place.
|
||||
For UIs with an already-bound collection that the DataSource manages in-place.
|
||||
|
||||
## Locking Strategy
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Remote Log Writers
|
||||
|
||||
Client-side log writers that send log data to remote endpoints. Source: `Loggers/` in this project. For core logging framework see `AyCode.Core/AyCode.Core/docs/LOGGING/README.md`. For server-side GlobalLogger see `AyCode.Core.Server/docs/LOGGING/README.md`.
|
||||
Client-side log writers sending log data to remote endpoints. Source: `Loggers/`. Core logging framework: `AyCode.Core/AyCode.Core/docs/LOGGING/README.md` | Server-side GlobalLogger: `AyCode.Core.Server/docs/LOGGING/README.md`.
|
||||
|
||||
## AcBrowserConsoleLogWriter
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ AcSignaRClientLogItemWriter AcLoggerSignalRHub<TLogger>
|
|||
│ │ └─ ... (all server writers)
|
||||
```
|
||||
|
||||
Note: `AcLoggerSignalRHub` overrides the client's `TimeStampUtc` with `DateTime.UtcNow` on the server side for authoritative timestamps. The hub is a simple `Hub` (not `AcWebSignalRHubBase`) — it does NOT use tag-based dispatch.
|
||||
Note: `AcLoggerSignalRHub` overrides client's `TimeStampUtc` with `DateTime.UtcNow` server-side for authoritative timestamps. The hub is a simple `Hub` (not `AcWebSignalRHubBase`) — does NOT use tag-based dispatch.
|
||||
|
||||
## Key Source Files
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# AyCode.Services documentation
|
||||
|
||||
Topic documentation for the `AyCode.Services` project (Layer 0, service abstractions).
|
||||
Topic docs for the `AyCode.Services` project (Layer 0, service abstractions).
|
||||
|
||||
## Topics
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ Topic documentation for the `AyCode.Services` project (Layer 0, service abstract
|
|||
|
||||
## Navigation
|
||||
|
||||
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Each topic folder has its own `README.md` with the main content, plus optional `TOPIC_ISSUES.md` (known issues) and `TOPIC_TODO.md` (planned work).
|
||||
Per the folder-navigation rule, start here when browsing `docs/`. Each topic folder has its own `README.md` (main content) + optional `TOPIC_ISSUES.md` and `TOPIC_TODO.md`.
|
||||
|
||||
## See also
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Framework vs. Consumer Boundary
|
||||
|
||||
This solution is **Layer 0 — Core framework**. Consumers (plural, unknown) reference it.
|
||||
Layer 0 (Core framework). Consumers (plural, unknown) reference downward only.
|
||||
|
||||
### Layer hierarchy
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ Layer 2 — Domain framework e.g. Mango.Nop.Core — NopCommerce-plugin
|
|||
Layer 3 — Consumer application the actual business app
|
||||
```
|
||||
|
||||
**Dependencies flow downward only.** A consumer CAN reference this framework; this framework CAN NEVER reference a consumer.
|
||||
**Dependencies flow downward only** — framework never references consumer.
|
||||
|
||||
### What belongs here vs. in a consumer
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ Layer 3 — Consumer application the actual business app
|
|||
|
||||
### Minimum-boilerplate ideal
|
||||
|
||||
Well-designed framework → minimal consumer setup. Aim for:
|
||||
Aim for minimal consumer setup:
|
||||
|
||||
```csharp
|
||||
// Consumer Program.cs — ideal pattern
|
||||
|
|
@ -38,7 +38,7 @@ services.Configure<AcXxxOptions>(config.GetSection("AyCode:Xxx"));
|
|||
services.AddAcXxxFactory<MyConcreteType>();
|
||||
```
|
||||
|
||||
Verbose consumer code = framework incomplete. Promote recurring patterns via extension methods, default-providing base classes, or options classes.
|
||||
Verbose consumer code → incomplete framework. Promote recurring patterns to extension methods, base classes, or options.
|
||||
|
||||
### Promotion pattern
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ When a pattern appears in 2+ consumer projects:
|
|||
2. Move generic part → appropriate framework layer (abstract base, options, or extension)
|
||||
3. Leave specific part in consumer (override or configure)
|
||||
|
||||
Framework design follows **"write the base first, derive the specific later"** — when planning a new feature, first consider whether the generic part fits the framework, only then implement consumer-specific derived code.
|
||||
Pattern: **"write the base first, derive the specific later"** — plan the framework abstraction before consumer-specific code.
|
||||
|
||||
### Class prefix — framework-only mandate
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ Framework design follows **"write the base first, derive the specific later"**
|
|||
|
||||
**Product/Consumer repos** (`@repo.type = "product"` or `"consumer"`) **MUST NOT** prefix their domain types. Product types are concrete derivations of framework abstractions and use natural domain names (e.g. `Order`, `ShippingItem`, `OrderItemPallet` — no product-specific prefix).
|
||||
|
||||
**Why:** the prefix marks code as **framework-bequeathed**. When a product class is named `Order`, an LLM (or human) reading consuming code immediately knows: this is product-domain concrete code, not framework abstraction. Cross-repo reference patterns become self-documenting: `AcDalBase<Order>` reads as "framework abstraction parameterized by a product concrete type" without needing to look up either side.
|
||||
**Why:** prefix marks code as framework-bequeathed. `Order` (un-prefixed) = product concrete; `AcDalBase` = framework abstraction. Cross-repo patterns self-document: `AcDalBase<Order>` reads as "framework abstraction over product concrete" without lookup.
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ AyCode.Database ─────────────────────
|
|||
AyCode.Services ← AyCode.Services.Server
|
||||
```
|
||||
|
||||
**Rule:** Dependencies flow upward only. Lower layers never reference higher layers.
|
||||
**Rule:** Dependencies flow upward only — lower layers never reference higher.
|
||||
|
||||
## Project Roles
|
||||
|
||||
|
|
@ -105,9 +105,7 @@ AyCode.Services ← AyCode.Services.Server
|
|||
|
||||
## Project Layout — Shared/Server Split
|
||||
|
||||
**Workspace-wide convention** — applies to AyCode.* family, Mango.Nop.* family, FruitBank, FruitBankHybridApp, and all future projects in this workspace.
|
||||
|
||||
Each logical project gets one or two physical projects, named by suffix:
|
||||
**Workspace-wide convention.** Each logical project gets one or two physical projects, named by suffix:
|
||||
|
||||
| Suffix | Contains | Visibility |
|
||||
|--------------|-------------------------------------------------------------------|--------------|
|
||||
|
|
@ -117,21 +115,21 @@ Each logical project gets one or two physical projects, named by suffix:
|
|||
|
||||
**Examples already in this repo:** `AyCode.Core` + `AyCode.Core.Server`, `AyCode.Interfaces` + `AyCode.Interfaces.Server`, `AyCode.Entities` + `AyCode.Entities.Server`, `AyCode.Models` + `AyCode.Models.Server`, `AyCode.Services` + `AyCode.Services.Server`. **No `.Client` projects exist by default.**
|
||||
|
||||
### Why no symmetric `.Client` by default
|
||||
### Why no symmetric `.Client`
|
||||
|
||||
- Most code is genuinely shared (DTOs, common logic). A separate `.Client` project for the rare client-only case keeps the workspace project count manageable.
|
||||
- Project-count discipline: doubling every shared project to a `.Client` + `.Server` pair would balloon the workspace without proportional benefit.
|
||||
- Create `.Client` only when client-only code exists that doesn't belong in the shared `Foo` (rare).
|
||||
- Most client-relevant code is genuinely shared (DTOs, common logic).
|
||||
- Doubling every project to `.Client` + `.Server` would balloon the workspace without proportional benefit.
|
||||
- Create `.Client` only for true client-only code (rare).
|
||||
|
||||
### Why no security risk in shared code seeing server context
|
||||
### Why no security risk
|
||||
|
||||
- The boundary is **directional**: server may reference shared code; client must not reference `.Server` code.
|
||||
- Enforcement is at the **project-graph + DLL reference level**, not the suffix. A client project that accidentally references `Foo.Server` will fail to build — manual review catches the rest.
|
||||
- A server seeing the shared (client-relevant) code is fine — the shared code is, by design, safe to expose to both sides.
|
||||
- The boundary is **directional**: server may reference shared; client must not reference `.Server`.
|
||||
- Enforcement at the **project-graph + DLL reference level**, not the suffix — a client referencing `.Server` fails to build.
|
||||
- Shared code is by design safe for both sides.
|
||||
|
||||
### When a new project is added
|
||||
|
||||
Default: **one shared `Foo` project**. Add `Foo.Server` only when server-only code accumulates that genuinely cannot live shared. Add `Foo.Client` only in the rare case described above.
|
||||
Default: **one shared `Foo` project**. Add `Foo.Server` when server-only code accumulates. Add `Foo.Client` only for the rare case above.
|
||||
|
||||
## Serialization Architecture
|
||||
|
||||
|
|
@ -158,4 +156,4 @@ abstract class AcUser<TProfile, TCompany, TUserToCompany, TAddress> { ... }
|
|||
class User : AcUser<Profile, Company, UserToCompany, Address> { ... }
|
||||
```
|
||||
|
||||
This allows the framework to define relationships without knowing concrete types.
|
||||
Framework defines relationships without knowing concrete types.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Bearer-token user authentication: JWT issuance, client-side token storage, HTTP
|
|||
|
||||
## Status
|
||||
|
||||
⚠️ **Pre-implementation.** ADR 0001 specifies the architecture; the framework code has not yet landed. This README is a scaffold — the "Consumer recipe" section is a placeholder for content that fills in as the implementation progresses (per ADR 0001 Follow-up "Implementation" series).
|
||||
⚠️ **Pre-implementation.** ADR 0001 specifies the architecture; framework code not yet landed. This README is a scaffold — the "Consumer recipe" section fills in as implementation progresses (per ADR 0001 Follow-up "Implementation" series).
|
||||
|
||||
## Scope
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ Sections to come (mirrors ADR 0001 Follow-up "Implementation" + "Consumer wiring
|
|||
|
||||
JWT signing keys, access tokens, refresh tokens, password hashes, and OAuth client secrets MUST NEVER appear in log output. Logged secrets leak via the same channels as the logs themselves (file system, retention archives, screenshots, bug reports). For diagnostics, log only metadata (user ID, expiry, issuer) or hash prefixes — never the raw value.
|
||||
|
||||
This guideline emerged from `ACCORE-LOG-I-P5W3` and `ACCORE-LOG-I-K1Z7` (both `Closed (2026-04-25)` via `#if DEBUG` gating per ADR 0001 pre-flight). A pending TODO (forthcoming entry in `LOGGING_TODO.md`, no ID assigned yet) proposes adding a generalized framework-level guideline section to [`LOGGING/README.md`](../../AyCode.Core/docs/LOGGING/README.md); when that lands, it becomes the canonical home and this section trims to a cross-ref.
|
||||
This guideline emerged from `ACCORE-LOG-I-P5W3` and `ACCORE-LOG-I-K1Z7` (both `Closed (2026-04-25)` via `#if DEBUG` gating per ADR 0001 pre-flight). A pending TODO (forthcoming entry in `LOGGING_TODO.md`, no ID assigned yet) proposes a generalized framework-level guideline section in [`LOGGING/README.md`](../../AyCode.Core/docs/LOGGING/README.md); when it lands, this section trims to a cross-ref.
|
||||
|
||||
## Cross-references
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
# AyCode.Core documentation
|
||||
|
||||
Top-level documentation for the `AyCode.Core` repo (Layer 0 — core framework).
|
||||
Top-level docs for `AyCode.Core` (Layer 0 — core framework).
|
||||
|
||||
## Reference docs (flat)
|
||||
|
||||
|
|
@ -17,4 +17,4 @@ Top-level documentation for the `AyCode.Core` repo (Layer 0 — core framework).
|
|||
|
||||
## Navigation
|
||||
|
||||
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Single-file reference docs remain flat at the repo-root level; multi-file topics live in named subfolders at the sub-project level.
|
||||
Per the folder-navigation rule, start here when browsing `docs/`. Flat single-file reference docs at repo-root; multi-file topics in named subfolders at sub-project level.
|
||||
|
|
|
|||
Loading…
Reference in New Issue