From 3b45de6de351dfe3615183020626530e35123880 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 1 May 2026 14:01:23 +0200 Subject: [PATCH] [LOADED_DOCS: 3 files, no new loads] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- AyCode.Core.Serializers.Console/Program.cs | 198 +++++++++++------- AyCode.Core.Server/docs/LOGGING/README.md | 2 +- AyCode.Core.Server/docs/README.md | 4 +- .../TestModels/SharedTestModels.cs | 48 ++--- AyCode.Core/Serializers/README.md | 4 +- AyCode.Core/docs/BINARY/BINARY_FEATURES.md | 6 +- AyCode.Core/docs/BINARY/BINARY_FORMAT.md | 2 +- .../docs/BINARY/BINARY_IMPLEMENTATION.md | 12 +- AyCode.Core/docs/BINARY/BINARY_OPTIONS.md | 14 +- AyCode.Core/docs/BINARY/BINARY_SGEN.md | 8 +- AyCode.Core/docs/BINARY/BINARY_WRITERS.md | 8 +- AyCode.Core/docs/BINARY/README.md | 6 +- AyCode.Core/docs/LOGGING/README.md | 74 ++----- AyCode.Core/docs/README.md | 4 +- AyCode.Core/docs/TOON/README.md | 46 ++-- AyCode.Core/docs/TOON/TOON_FORMAT.md | 4 +- AyCode.Core/docs/TOON/TOON_IMPLEMENTATION.md | 4 +- AyCode.Core/docs/TOON/TOON_INFERENCE.md | 6 +- AyCode.Core/docs/TOON/TOON_OPTIONS.md | 4 +- AyCode.Core/docs/XCUT/README.md | 2 +- AyCode.Services.Server/docs/README.md | 4 +- AyCode.Services.Server/docs/SIGNALR/README.md | 2 +- .../docs/SIGNALR_DATASOURCE/README.md | 12 +- AyCode.Services/docs/LOGGING/README.md | 4 +- AyCode.Services/docs/README.md | 4 +- AyCode.Services/docs/SIGNALR/README.md | 11 +- .../docs/SIGNALR_BINARY_PROTOCOL/README.md | 14 +- docs/ARCHITECTURE.md | 38 ++-- docs/AUTH/README.md | 4 +- docs/CONVENTIONS.md | 12 +- docs/GLOSSARY.md | 24 +-- docs/README.md | 4 +- 32 files changed, 295 insertions(+), 294 deletions(-) diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index fe28876..a93924e 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -113,10 +113,29 @@ public static class Program $"Compression={options.UseCompression}{extra}"; } + /// + /// Converts a total-time (in ms across ) into per-operation microseconds. + /// Formula: totalMs / iterations × 1000. The benchmark stores *TimeMs as the cumulative + /// median over the timing run; the display layer renders per-op µs to make numbers iteration-count + /// independent (e.g. switching TestIterations 1000 → 100 leaves the displayed µs/op unchanged + /// — only its sample noise grows). Symmetric with the already-per-op *AllocBytesPerOp fields. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double ToPerOpMicros(double totalMs) => totalMs / TestIterations * 1000.0; + + /// + /// Converts a byte count to KB (1 KB = 1024 B). Display-only helper so allocation columns can + /// render compact F2 KB values (e.g. 4.05 KB instead of 4,144 B) — header carries + /// the unit so per-row entries stay numbers-only. CSV / raw-data outputs keep the precise byte + /// integers untouched. + /// + [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; - /// One-time setup allocation cost (e.g., pre-allocated ArrayBufferWriter with internal buffer). Captured in constructor; 0 for byte[] API and Fresh-BufWriter variants. - long SetupAllocBytes { get; } + /// 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. + long SetupSerializeAllocBytes { get; } + /// 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. + long SetupDeserializeAllocBytes { get; } /// 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(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(_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(_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; } = ""; /// 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. + /// 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. public bool IsRoundTripOnly { get; set; } /// 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). 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; } /// Total round-trip time. For in-memory benchmarks: Serialize + Deserialize (set explicitly in /// RunBenchmarksForTestData). For round-trip-only benchmarks (NamedPipe etc.): the directly-measured /// pipe round-trip time, since Ser and Des are not separately measurable there. @@ -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 results, List 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}"); } } diff --git a/AyCode.Core.Server/docs/LOGGING/README.md b/AyCode.Core.Server/docs/LOGGING/README.md index 63bcbdb..4ed896c 100644 --- a/AyCode.Core.Server/docs/LOGGING/README.md +++ b/AyCode.Core.Server/docs/LOGGING/README.md @@ -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 diff --git a/AyCode.Core.Server/docs/README.md b/AyCode.Core.Server/docs/README.md index f0339ce..cf2edba 100644 --- a/AyCode.Core.Server/docs/README.md +++ b/AyCode.Core.Server/docs/README.md @@ -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 diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index 01a94fc..78dd3e7 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -55,7 +55,7 @@ public enum TestUserRole /// Implements IId<int> for semantic $id/$ref serialization. /// [MemoryPackable] -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class SharedTag : IId { @@ -80,7 +80,7 @@ public partial class SharedTag : IId /// Shared category - for hierarchical cross-reference testing. /// [MemoryPackable] -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class SharedCategory : IId { @@ -106,7 +106,7 @@ public partial class SharedCategory : IId /// Shared user reference - appears in many places to test $ref deduplication. /// [MemoryPackable] -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class SharedUser : IId { @@ -136,7 +136,7 @@ public partial class SharedUser : IId /// User preferences - non-IId nested object /// [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. /// [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 /// [MemoryPackable] -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestOrder : IId { @@ -249,7 +249,7 @@ public partial class TestOrder : IId /// /// Level 1: Main order - root of the hierarchy /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public partial class TestOrder_Circ_Ref : IId { public int Id { get; set; } @@ -284,7 +284,7 @@ public partial class TestOrder_Circ_Ref : IId /// Level 2: Order item with pallets /// [MemoryPackable] -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestOrderItem : IId { @@ -323,7 +323,7 @@ public partial class TestOrderItem : IId /// /// Level 2: Order item with pallets /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public partial class TestOrderItem_Circ_Ref : IId { public int Id { get; set; } @@ -347,7 +347,7 @@ public partial class TestOrderItem_Circ_Ref : IId /// Level 3: Pallet containing measurements /// [MemoryPackable] -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestPallet : IId { @@ -390,7 +390,7 @@ public partial class TestPallet : IId /// Level 4: Measurement with multiple points /// [MemoryPackable] -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestMeasurement : IId { @@ -425,7 +425,7 @@ public partial class TestMeasurement : IId /// Level 5: Deepest level - measurement point /// [MemoryPackable] -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestMeasurementPoint : IId { @@ -459,7 +459,7 @@ public partial class TestMeasurementPoint : IId /// /// Order with Guid Id - for testing Guid-based IId /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public class TestGuidOrder : IId { public Guid Id { get; set; } @@ -471,7 +471,7 @@ public class TestGuidOrder : IId /// /// Item with Guid Id /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public class TestGuidItem : IId { public Guid Id { get; set; } @@ -487,7 +487,7 @@ public class TestGuidItem : IId /// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values /// are stored as strings in the database. /// -[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. /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public class TestDtoWithGenericAttributes : IId { public int Id { get; set; } @@ -510,7 +510,7 @@ public class TestDtoWithGenericAttributes : IId /// /// Order with nullable collections for null vs empty testing /// -[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 /// [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. /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public class ExtendedPrimitiveTestClass { public int Id { get; set; } @@ -576,7 +576,7 @@ public class ExtendedPrimitiveTestClass /// /// Class with array of objects containing null items for WriteNull coverage /// -[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. /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public class ServerCustomerDto : IId { public int Id { get; set; } @@ -624,7 +624,7 @@ public class ServerCustomerDto : IId /// the deserializer must skip unknown properties correctly /// while still maintaining string intern table consistency. /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public class ClientCustomerDto : IId { public int Id { get; set; } @@ -638,7 +638,7 @@ public class ClientCustomerDto : IId /// Server DTO with nested objects that client doesn't know about. /// Tests skipping complex nested structures. /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public class ServerOrderWithExtras : IId { public int Id { get; set; } @@ -659,7 +659,7 @@ public class ServerOrderWithExtras : IId /// /// Client version of the order - doesn't have Customer/RelatedCustomers properties. /// -[AcBinarySerializable(false, true, false, false)] +[AcBinarySerializable(false)] public class ClientOrderSimple : IId { public int Id { get; set; } diff --git a/AyCode.Core/Serializers/README.md b/AyCode.Core/Serializers/README.md index 2fa239f..252a283 100644 --- a/AyCode.Core/Serializers/README.md +++ b/AyCode.Core/Serializers/README.md @@ -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). diff --git a/AyCode.Core/docs/BINARY/BINARY_FEATURES.md b/AyCode.Core/docs/BINARY/BINARY_FEATURES.md index 89beec5..13af74a 100644 --- a/AyCode.Core/docs/BINARY/BINARY_FEATURES.md +++ b/AyCode.Core/docs/BINARY/BINARY_FEATURES.md @@ -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. diff --git a/AyCode.Core/docs/BINARY/BINARY_FORMAT.md b/AyCode.Core/docs/BINARY/BINARY_FORMAT.md index 9a171e1..9ba3ba4 100644 --- a/AyCode.Core/docs/BINARY/BINARY_FORMAT.md +++ b/AyCode.Core/docs/BINARY/BINARY_FORMAT.md @@ -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) diff --git a/AyCode.Core/docs/BINARY/BINARY_IMPLEMENTATION.md b/AyCode.Core/docs/BINARY/BINARY_IMPLEMENTATION.md index 46d9a05..0cd89b1 100644 --- a/AyCode.Core/docs/BINARY/BINARY_IMPLEMENTATION.md +++ b/AyCode.Core/docs/BINARY/BINARY_IMPLEMENTATION.md @@ -57,7 +57,7 @@ SGen fast path: NO → full path (IQueryable/Expression + WriteValue dispatch) ``` -**Why safe:** GeneratedWriter exists → type is NOT IQueryable/Expression/primitive/byte[]/IDictionary/IEnumerable. SGen only generates for object model types. WriteObject handles FixObj slot assignment, UseMetadata, RefHandling — all work correctly when called directly with pre-resolved wrapper. +**Why safe:** GeneratedWriter exists → type is NOT IQueryable/Expression/primitive/byte[]/IDictionary/IEnumerable. SGen only generates for object model types. WriteObject handles FixObj slot assignment, UseMetadata, RefHandling correctly when called with pre-resolved wrapper. **Saved overhead:** `is IQueryable` interface check, `IsExpressionType` IsAssignableFrom, `TryWritePrimitive` Type.GetTypeCode + 15-case switch, `WriteValueNonPrimitive` 4 interface checks + dupla GetWrapper, 2 eliminated method call levels. @@ -90,7 +90,7 @@ When `UseMetadata = false`: no inline property name hashes needed. SGen bypasses ## High-Performance Coding Rules -Strictly enforced within serialization pipeline. AI agents and developers MUST follow. +Strictly enforced. AI agents and developers MUST follow. ### 1. The "Write Plan" (O(1) Reference Tracking) @@ -117,11 +117,11 @@ Two-phase: **Inlining barriers — `[MethodImpl(AggressiveInlining)]` is silently ignored when:** -- **`try` / `catch` / `finally` / `using`** — on **.NET 9 (project's minimum target, see `copilot-instructions.md` rule 16)** any EH region is a hard JIT inlining barrier (`inline.cpp` in CoreCLR). `using` statements desugar to `try/finally` and have the same effect. Move resource cleanup (`Pool.Return`, `ArrayPool.Return`, `Dispose`) into a separate cold method or keep the cleanup outside the hot caller. The Pool.Get → try/finally → Pool.Return pattern (Rule #5) is fine because it sits at the entry point of `Serialize`, not on a per-property hot path. — **Treat as a hard rule regardless of runtime.** .NET 10 partially lifts the restriction for same-module try-finally ([`dotnet/runtime#112998`](https://github.com/dotnet/runtime/pull/112998), merged 2025-03-20), but `catch`, cross-module try-finally, and P/Invoke-stub cases stay blocked even there. Code in this repo must run inline-friendly on .NET 9 today AND on .NET 10+ tomorrow — staying EH-free is the portable, conservative choice. Audit scope and rationale: `BINARY_TODO.md#accore-bin-t-t5j8`. -- **`stackalloc` with non-constant or large size** — small constant `stackalloc` (≤ ~1KB) is inlinable in .NET 6+, but the moment any other barrier (try/finally, complex control flow) is added the method becomes non-inlinable. When mixing `stackalloc` with `try/finally` (e.g. ArrayPool fallback + scratch buffer), expect the helper to always be a separate call frame — design accordingly (avoid inline-only assumptions in the caller). +- **`try` / `catch` / `finally` / `using`** — on **.NET 9** (project minimum, see `copilot-instructions.md` rule 16) any EH region is a hard JIT inlining barrier (`inline.cpp` in CoreCLR). `using` desugars to `try/finally` with the same effect. Move resource cleanup (`Pool.Return`, `ArrayPool.Return`, `Dispose`) into a separate cold method, or keep it outside the hot caller. The Pool.Get → try/finally → Pool.Return pattern (Rule #5) sits at the entry of `Serialize`, not on a per-property hot path. — **Hard rule regardless of runtime.** .NET 10 partially lifts the restriction for same-module try-finally ([`dotnet/runtime#112998`](https://github.com/dotnet/runtime/pull/112998), merged 2025-03-20), but `catch`, cross-module try-finally, and P/Invoke-stub cases stay blocked. Code must run inline-friendly on .NET 9 today AND .NET 10+ tomorrow — staying EH-free is the portable choice. Audit: `BINARY_TODO.md#accore-bin-t-t5j8`. +- **`stackalloc` with non-constant or large size** — small constant `stackalloc` (≤ ~1KB) is inlinable in .NET 6+, but adding any other barrier (try/finally, complex control flow) makes the method non-inlinable. When mixing `stackalloc` with `try/finally` (e.g. ArrayPool fallback + scratch buffer), expect a separate call frame — avoid inline-only assumptions in the caller. - **Method size / IL token count** — the JIT has IL-size and basic-block thresholds even with `AggressiveInlining`. For large generated methods (SGen `WriteProperties` for property-heavy types) the attribute is a hint, not a guarantee; see `BINARY_TODO.md#accore-bin-t-t5j8` for `AggressiveOptimization` as a complementary tool. -When adding a new helper to the hot path: read the method first for any of the above before placing `[MethodImpl(AggressiveInlining)]` on it. The attribute lies if the body has an EH region — silently. +When adding a new helper to the hot path: check for any of the above before placing `[MethodImpl(AggressiveInlining)]`. The attribute silently lies if the body has an EH region. ### 4. SGen Root Fast Path @@ -151,7 +151,7 @@ When adding a new helper to the hot path: read the method first for any of the a ## Metadata Lifecycle & Cold-Start (planned: ACCORE-BIN-T-W9F1 / ACCORE-BIN-T-T5J8) -Today `BinarySerializeTypeMetadata` and `BinaryDeserializeTypeMetadata` are built lazily in `GetWrapperSlow` via `GlobalMetadataCache.GetOrAdd(type, MetadataFactory)`. The factory runs reflection property enumeration, attribute scans, and `Expression.Compile` per property — the dominant first-call cost for SGen types (see `BINARY_ISSUES.md#accore-bin-i-n6q3`). +Today `BinarySerializeTypeMetadata` / `BinaryDeserializeTypeMetadata` are built lazily in `GetWrapperSlow` via `GlobalMetadataCache.GetOrAdd(type, MetadataFactory)`. The factory runs reflection property enumeration, attribute scans, `Expression.Compile` per property — dominant first-call cost for SGen types (see `BINARY_ISSUES.md#accore-bin-i-n6q3`). **Planned evolution** (`BINARY_TODO.md#accore-bin-t-w9f1`): diff --git a/AyCode.Core/docs/BINARY/BINARY_OPTIONS.md b/AyCode.Core/docs/BINARY/BINARY_OPTIONS.md index 12ee14d..eee9e07 100644 --- a/AyCode.Core/docs/BINARY/BINARY_OPTIONS.md +++ b/AyCode.Core/docs/BINARY/BINARY_OPTIONS.md @@ -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 | |-------------|--------| diff --git a/AyCode.Core/docs/BINARY/BINARY_SGEN.md b/AyCode.Core/docs/BINARY/BINARY_SGEN.md index e84cac1..bb61f24 100644 --- a/AyCode.Core/docs/BINARY/BINARY_SGEN.md +++ b/AyCode.Core/docs/BINARY/BINARY_SGEN.md @@ -73,7 +73,7 @@ SGen Root (WriteProperties) └─ Runtime grandchild → WriteObjectGenerated (bridge → GetWrapper + WriteObject) ``` -**Key:** SGen types can call **directly** into other SGen types' `WriteProperties` (zero dispatch). Non-SGen children fall back to runtime via bridge methods (full type dispatch). +**Key:** SGen types can call **directly** into other SGen types' `WriteProperties` (zero dispatch). Non-SGen children fall back to runtime via bridge methods. ## Bridge Methods @@ -138,7 +138,7 @@ void ScanObject(object value, BinarySerializationContext ctx, Design rules for anyone modifying `AcBinarySourceGenerator.cs`. Violating these silently regresses the SGen hot path. -**No EH regions in generated hot methods.** Generated `WriteProperties` / `ScanObject` / `ScanForDuplicates` / `ReadObject` / `ReadProperties` MUST NOT emit `try`, `catch`, `finally`, or `using` blocks. On **.NET 9 (project's minimum target)**, the CoreCLR JIT refuses to inline any method containing an EH region — `AggressiveInlining` is silently ignored, and the SGen Root Fast Path collapses back to a regular call frame at every property write. .NET 10 partially lifts this for same-module try-finally ([`dotnet/runtime#112998`](https://github.com/dotnet/runtime/pull/112998), merged 2025-03-20); however, this is **not yet our minimum runtime**, and even on .NET 10+ `catch` / cross-module / P/Invoke-stub cases remain blocked. Treat this as a hard rule for the SGen output regardless of runtime — the generated method must work on .NET 9 today AND keep its inline-friendliness on .NET 10+ tomorrow. +**No EH regions in generated hot methods.** Generated `WriteProperties` / `ScanObject` / `ScanForDuplicates` / `ReadObject` / `ReadProperties` MUST NOT emit `try`, `catch`, `finally`, or `using` blocks. On **.NET 9** (project minimum), the CoreCLR JIT refuses to inline any method with an EH region — `AggressiveInlining` is silently ignored, and the SGen Root Fast Path collapses to a regular call frame per property write. .NET 10 partially lifts this for same-module try-finally ([`dotnet/runtime#112998`](https://github.com/dotnet/runtime/pull/112998), merged 2025-03-20); not yet our minimum runtime, and `catch` / cross-module / P/Invoke-stub cases remain blocked even on .NET 10+. Hard rule for SGen output regardless of runtime — generated method must work on .NET 9 AND keep inline-friendliness on .NET 10+. **Future-feature trap.** When adding generator features that *seem* to need EH: @@ -147,9 +147,9 @@ Design rules for anyone modifying `AcBinarySourceGenerator.cs`. Violating these - Validation attributes (`[Validate]`, `[Required]`) → cold helper that throws; the generated method calls it without try/catch. - Rented-buffer cleanup (`using var pooled = ...`) → keep the `using` in the entry frame (`Serialize`), never in the generated `WriteProperties`. -**Straight-line rule.** Generated hot methods are: `Unsafe.As` cast → null/depth check → property write or bridge call → repeat. No exception handling, no resource ownership, no early-return cleanup. Resource ownership lives at `Serialize` / `Deserialize` entry, not per-property. +**Straight-line rule.** Generated hot methods: `Unsafe.As` cast → null/depth check → property write or bridge call → repeat. No exception handling, no resource ownership, no early-return cleanup. Resource ownership lives at `Serialize` / `Deserialize` entry, not per-property. -See `BINARY_IMPLEMENTATION.md` Rule #3 (Inlining barriers) for the JIT-level rationale and `BINARY_TODO.md#accore-bin-t-t5j8` for the audit task that enforces this on the existing generator output. +See `BINARY_IMPLEMENTATION.md` Rule #3 (Inlining barriers) for JIT-level rationale; `BINARY_TODO.md#accore-bin-t-t5j8` for the audit task enforcing this on existing output. ## Object Marker Bridge — Metadata Caching diff --git a/AyCode.Core/docs/BINARY/BINARY_WRITERS.md b/AyCode.Core/docs/BINARY/BINARY_WRITERS.md index 592e9e6..b382700 100644 --- a/AyCode.Core/docs/BINARY/BINARY_WRITERS.md +++ b/AyCode.Core/docs/BINARY/BINARY_WRITERS.md @@ -61,7 +61,7 @@ Context/standalone share only `IBufferWriter` ref and `_committedBytes`. ### Known Limitations -Buffer-writer contract limitations are tracked in `BINARY_ISSUES.md` under the **Buffer Writer (BWO)** category — struct copy semantics, init-reset tracking, ctor chunk acquisition, no-mode-mixing rule. +Buffer-writer contract limitations: `BINARY_ISSUES.md` under **Buffer Writer (BWO)** category — struct copy semantics, init-reset tracking, ctor chunk acquisition, no-mode-mixing rule. ### Chunk Size @@ -73,7 +73,7 @@ Default 65536 (64KB), configurable via `AcBinarySerializerOptions.BufferWriterCh ## Why Writes Are on the Context -Most important architectural decision in the output layer. +Key architectural decision in the output layer. **Attempted:** write methods on output struct. Context calls `Output.WriteByte(value)`. **Result:** measurably slower, even with struct + `AggressiveInlining` + generic constraint devirtualization. @@ -96,7 +96,7 @@ void Release(); - **ArrayBinaryInput:** single `byte[]`, `TryAdvanceSegment => false` (JIT-eliminated), `Release` no-op. - **SequenceBinaryInput:** lazy `TryGet` iteration over `ReadOnlySequence`. Context `_buffer` points to segment backing `byte[]` (zero-copy). Cross-boundary: `ArrayPool` scratch, N-segment loop. `Release` returns scratch to pool. -- **PipeReaderBinaryInput:** reads from `PipeReader` with on-demand data via `ReadAsync`. Same cross-boundary pattern as `SequenceBinaryInput`, but when all segments in the current `ReadResult` are exhausted, calls `AdvanceTo` + `ReadAsync().GetAwaiter().GetResult()` to get more data from the pipe. Enables pipeline parallelism with `AsyncPipeWriterOutput`: deserializer processes chunks as they arrive from the network instead of waiting for the full payload. `Release` returns scratch to pool + signals pipe consumption via `AdvanceTo`. +- **PipeReaderBinaryInput:** reads from `PipeReader` with on-demand data via `ReadAsync`. Same cross-boundary pattern as `SequenceBinaryInput`; when all segments in current `ReadResult` exhausted, calls `AdvanceTo` + `ReadAsync().GetAwaiter().GetResult()` for more data. Enables pipeline parallelism with `AsyncPipeWriterOutput`: deserializer processes chunks as they arrive, not after full payload. `Release` returns scratch + signals pipe consumption via `AdvanceTo`. ## AsyncPipeWriterOutput @@ -131,7 +131,7 @@ Max chunk data size: 65535 bytes (UINT16 max). ### Usage -Selected via `BinaryProtocolMode.AsyncSegment` in `AcBinaryHubProtocol`. The protocol's `WriteMessageChunked` method sends CHUNK_START (standard SignalR framing), then calls the serializer which writes all chunks via `AsyncPipeWriterOutput`, then the protocol writes `[202]`. +Selected via `BinaryProtocolMode.AsyncSegment` in `AcBinaryHubProtocol`. The protocol's `WriteMessageChunked` sends CHUNK_START (standard SignalR framing), the serializer writes all chunks via `AsyncPipeWriterOutput`, the protocol writes `[202]`. ```csharp AcBinarySerializer.Serialize(value, pipeWriter, options); diff --git a/AyCode.Core/docs/BINARY/README.md b/AyCode.Core/docs/BINARY/README.md index 44606a1..688d186 100644 --- a/AyCode.Core/docs/BINARY/README.md +++ b/AyCode.Core/docs/BINARY/README.md @@ -1,6 +1,6 @@ # BINARY — AcBinary serializer -Reference documentation for the AcBinary serialization system. Primary goal: **speed** (two-phase scan+serialize, reference tracking, string interning). +AcBinary serialization system. Primary goal: **speed** (two-phase scan+serialize, reference tracking, string interning). ## Files in this folder @@ -17,7 +17,7 @@ Reference documentation for the AcBinary serialization system. Primary goal: **s ## Start here -For a first read-through, start with [`BINARY_FEATURES.md`](BINARY_FEATURES.md) for the overview, then dive into [`BINARY_FORMAT.md`](BINARY_FORMAT.md) for wire-level details. [`BINARY_SGEN.md`](BINARY_SGEN.md) explains how the code-gen integrates at build time. +Start with [`BINARY_FEATURES.md`](BINARY_FEATURES.md) (overview), then [`BINARY_FORMAT.md`](BINARY_FORMAT.md) (wire-level). [`BINARY_SGEN.md`](BINARY_SGEN.md) covers build-time code-gen integration. ## Cross-references @@ -27,4 +27,4 @@ For a first read-through, start with [`BINARY_FEATURES.md`](BINARY_FEATURES.md) ## Related ADRs -- [`AyCode.Core/docs/adr/0003-acbinary-streaming-receive-architecture.md`](../../../docs/adr/0003-acbinary-streaming-receive-architecture.md) — *AcBinary streaming receive — AsyncPipeReaderInput unified primitive and transport-agnostic helpers* (Status: Proposed (2026-04-27)). Repo-level cross-cutting ADR establishing the receive-side streaming architecture and transport-agnostic helpers (NamedPipe + FileStream) for this serializer. `AsyncPipeReaderInput` (sealed class) consolidates today's `SegmentBufferReader` + `SegmentBufferReaderInput` pair into a single self-contained primitive; the `Async`-prefixed naming mirrors the existing `AsyncPipeWriterOutput` send-side primitive. Implementation tracked across `ACCORE-BIN-T-D6H4` / `M2K1` / `V7C9` / `A3T8` / `B5Y6` (Steps 1–5) in [`BINARY_ASYNCPIPE_TODO.md`](BINARY_ASYNCPIPE_TODO.md) and `ACCORE-SBP-T-G7T2` (Step 6) in `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/SIGNALR_BINARY_PROTOCOL_TODO.md`. +- [`AyCode.Core/docs/adr/0003-acbinary-streaming-receive-architecture.md`](../../../docs/adr/0003-acbinary-streaming-receive-architecture.md) — Receive-side streaming architecture (Status: Proposed 2026-04-27). Consolidates `SegmentBufferReader` + `SegmentBufferReaderInput` into a single `AsyncPipeReaderInput` primitive (mirrors send-side `AsyncPipeWriterOutput`); adds transport-agnostic helpers (NamedPipe + FileStream). Implementation: [`BINARY_ASYNCPIPE_TODO.md`](BINARY_ASYNCPIPE_TODO.md) Steps 1–5 + `SIGNALR_BINARY_PROTOCOL_TODO.md` Step 6. diff --git a/AyCode.Core/docs/LOGGING/README.md b/AyCode.Core/docs/LOGGING/README.md index 1f164e8..254272d 100644 --- a/AyCode.Core/docs/LOGGING/README.md +++ b/AyCode.Core/docs/LOGGING/README.md @@ -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(configuration.GetSection("AyCode:Logger")); // 2. Register writer(s) as DI singletons services.AddSingleton(); -// Or, for client-scoped marker: +// Client-scoped marker variant: // services.AddSingleton(); -// 3. Register the logger factory — registers Func singleton in DI +// 3. Register the logger factory (Func singleton in DI) services.AddAcLoggerFactory(); -// Or, with custom writer marker (pulls only TWriterBase-registered writers): +// Custom writer-marker overload — pulls only TWriterBase-registered writers: // services.AddAcLoggerFactory(); ``` -Consumers inject `Func` and invoke it with a category name to obtain category-scoped logger instances. +Consumers inject `Func` and invoke it with a category name. ### Appsettings.json shape @@ -154,39 +146,23 @@ Consumers inject `Func` 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` 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` uses `Activator.CreateInstance(typeof(TLogger), AppType, LogLevel, categoryName, writers)`. `TLogger` must expose a public constructor with signature: +`AddAcLoggerFactory` 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>(sp => category => new MyConcreteLogger(...)); ``` -### Writer-marker scoping (two overloads) - -- `AddAcLoggerFactory()` — all `IAcLogWriterBase`-registered writers go into the logger -- `AddAcLoggerFactory()` — 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` method maps MS log levels to AC methods: +`Log` 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` implements `ILoggerProvider` with a `ConcurrentDictionary` per-category cache. Factory function provided at registration. - -**Extension methods:** +`AcLoggerProvider` implements `ILoggerProvider` with a per-category `ConcurrentDictionary` 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` 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` (MS logging) uses `EventId.Name` if available, else `"Log"`. ## Key Source Files diff --git a/AyCode.Core/docs/README.md b/AyCode.Core/docs/README.md index 02e882c..92b0da0 100644 --- a/AyCode.Core/docs/README.md +++ b/AyCode.Core/docs/README.md @@ -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). diff --git a/AyCode.Core/docs/TOON/README.md b/AyCode.Core/docs/TOON/README.md index c375eb7..c4123e7 100644 --- a/AyCode.Core/docs/TOON/README.md +++ b/AyCode.Core/docs/TOON/README.md @@ -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" } } ``` diff --git a/AyCode.Core/docs/TOON/TOON_FORMAT.md b/AyCode.Core/docs/TOON/TOON_FORMAT.md index c990d5e..8046952 100644 --- a/AyCode.Core/docs/TOON/TOON_FORMAT.md +++ b/AyCode.Core/docs/TOON/TOON_FORMAT.md @@ -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`, `Dictionary`; 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 Name = "Alice" ``` -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 diff --git a/AyCode.Core/docs/TOON/TOON_IMPLEMENTATION.md b/AyCode.Core/docs/TOON/TOON_IMPLEMENTATION.md index 4a4e5ef..170a72d 100644 --- a/AyCode.Core/docs/TOON/TOON_IMPLEMENTATION.md +++ b/AyCode.Core/docs/TOON/TOON_IMPLEMENTATION.md @@ -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 (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 diff --git a/AyCode.Core/docs/TOON/TOON_INFERENCE.md b/AyCode.Core/docs/TOON/TOON_INFERENCE.md index 38b3e51..6ce5ff7 100644 --- a/AyCode.Core/docs/TOON/TOON_INFERENCE.md +++ b/AyCode.Core/docs/TOON/TOON_INFERENCE.md @@ -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 diff --git a/AyCode.Core/docs/TOON/TOON_OPTIONS.md b/AyCode.Core/docs/TOON/TOON_OPTIONS.md index d14f7cc..29374dc 100644 --- a/AyCode.Core/docs/TOON/TOON_OPTIONS.md +++ b/AyCode.Core/docs/TOON/TOON_OPTIONS.md @@ -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` diff --git a/AyCode.Core/docs/XCUT/README.md b/AyCode.Core/docs/XCUT/README.md index d58b2c5..142a30e 100644 --- a/AyCode.Core/docs/XCUT/README.md +++ b/AyCode.Core/docs/XCUT/README.md @@ -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 diff --git a/AyCode.Services.Server/docs/README.md b/AyCode.Services.Server/docs/README.md index 089e4ba..ac93635 100644 --- a/AyCode.Services.Server/docs/README.md +++ b/AyCode.Services.Server/docs/README.md @@ -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 diff --git a/AyCode.Services.Server/docs/SIGNALR/README.md b/AyCode.Services.Server/docs/SIGNALR/README.md index e1f5efe..1f2c70b 100644 --- a/AyCode.Services.Server/docs/SIGNALR/README.md +++ b/AyCode.Services.Server/docs/SIGNALR/README.md @@ -72,7 +72,7 @@ Reflection runs lazily per tag on first request, then results are cached statica ConcurrentDictionary Sessions ``` -`IAcSessionItem` requires `SessionId` property. Used for targeting messages to specific users/connections. +`IAcSessionItem` requires `SessionId` property. Targets messages to specific users/connections. ## Broadcast Service diff --git a/AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md b/AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md index fd6d530..2dcce86 100644 --- a/AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md +++ b/AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md @@ -7,7 +7,7 @@ Change-tracked real-time collection on top of the SignalR transport layer. Sourc ## Overview -`AcSignalRDataSource` is an **abstract** generic `IList` 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` 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 : IList, IList, IReadOnlyList @@ -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 diff --git a/AyCode.Services/docs/LOGGING/README.md b/AyCode.Services/docs/LOGGING/README.md index 0f92d54..ddecf62 100644 --- a/AyCode.Services/docs/LOGGING/README.md +++ b/AyCode.Services/docs/LOGGING/README.md @@ -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 │ │ └─ ... (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 diff --git a/AyCode.Services/docs/README.md b/AyCode.Services/docs/README.md index 7c5d111..de10cba 100644 --- a/AyCode.Services/docs/README.md +++ b/AyCode.Services/docs/README.md @@ -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 diff --git a/AyCode.Services/docs/SIGNALR/README.md b/AyCode.Services/docs/SIGNALR/README.md index 2555e9b..8b17918 100644 --- a/AyCode.Services/docs/SIGNALR/README.md +++ b/AyCode.Services/docs/SIGNALR/README.md @@ -14,8 +14,7 @@ Client ──OnReceiveMessage(tag, requestId, signalParams, data)──► Serve Client ◄──OnReceiveMessage(tag, requestId, signalParams, data)── Server ``` -Tag (int) determines server method. All calls go through `OnReceiveMessage`. -Metadata (`SignalParams`) and payload (`object data`) travel as **separate hub arguments** — `SignalParams` is AcBinary serialized normally, `data` is serialized directly to the pipe via `AcBinarySerializer` (zero-copy write) or passed through as `byte[]` via protocol fast-path. +Tag (int) selects server method; all calls go through `OnReceiveMessage`. Metadata (`SignalParams`) and payload (`object data`) travel as **separate hub args** — `SignalParams` AcBinary-serialized; `data` zero-copy to pipe via `AcBinarySerializer`, or raw `byte[]` via protocol fast-path. ``` Client: Server: @@ -70,9 +69,9 @@ CRUD helpers (`PostAsync`, `GetByIdAsync`, `GetAllAsync`, `PostDataAsync`) are g ### AcBinaryHubProtocol / AyCodeBinaryHubProtocol -Custom `IHubProtocol` (`"acbinary"`), replaces JSON. Zero-copy write via `BufferWriterBinaryOutput` standalone mode + `AcBinarySerializer.Serialize(value, output)` directly to pipe. Zero-copy read via `SequenceReader` from pipe's `ReadOnlySequence`. `BinaryProtocolMode` constructor parameter selects transport strategy: `Bytes` (default, ArrayBinaryOutput → byte[]), `Segment` (BWO zerocopy to PipeWriter, single flush), `AsyncSegment` (AsyncPipeWriterOutput, per-chunk FlushAsync + pipeline parallelism). +Custom `IHubProtocol` (`"acbinary"`) replacing JSON. Zero-copy write: `BufferWriterBinaryOutput` + `AcBinarySerializer.Serialize(value, output)` directly to pipe. Zero-copy read: `SequenceReader` from pipe's `ReadOnlySequence`. `BinaryProtocolMode` ctor param selects transport: `Bytes` (default, ArrayBinaryOutput → byte[]), `Segment` (BWO zero-copy to PipeWriter, single flush), `AsyncSegment` (AsyncPipeWriterOutput, per-chunk FlushAsync + pipeline parallelism). -`AcBinaryHubProtocol` is the base (unsealed) — general binary framing only. `AyCodeBinaryHubProtocol` derives from it with consumer-specific logic: `SignalParams` capture (via `OnArgumentRead` hook), `IsRawBytesData` path, `SignalDataType` type resolution. Register `AyCodeBinaryHubProtocol` in both client and server. +`AcBinaryHubProtocol` is the unsealed base — general binary framing. `AyCodeBinaryHubProtocol` derives with consumer-specific logic: `SignalParams` capture (`OnArgumentRead` hook), `IsRawBytesData` path, `SignalDataType` resolution. Register `AyCodeBinaryHubProtocol` on both client and server. > Wire format, argument framing, dual BWO pattern, length prefix patching: `../SIGNALR_BINARY_PROTOCOL/README.md` @@ -150,9 +149,9 @@ GetParameterValues(ParameterInfo[]): Hub validates: missing required params throw ArgumentException. ``` -Type-guided deserialization — each parameter is individually serialized/deserialized with its concrete type, avoiding the `object[]` → dictionary problem of untyped binary deserialization. +Type-guided — each parameter individually serialized/deserialized with its concrete type, avoiding the `object[]` → dictionary problem of untyped binary deserialization. -> Known concerns and limitations on parameter serialization (per-parameter overhead, AcBinary-only) are tracked in `SIGNALR_ISSUES.md` under `ACCORE-SIG-I-L5K3` and `ACCORE-SIG-I-H8D6`. +> Known concerns on parameter serialization (per-parameter overhead, AcBinary-only): `SIGNALR_ISSUES.md` under `ACCORE-SIG-I-L5K3` and `ACCORE-SIG-I-H8D6`. ## Response Patterns diff --git a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md index 126d2bd..2fbfaf3 100644 --- a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md +++ b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md @@ -142,17 +142,17 @@ Zero-copy when possible: if single-segment and backing array matches exactly → ### SequenceBinaryInput (Multi-Segment Deserialization) -`struct SequenceBinaryInput : IBinaryInputBase` — reads from `ReadOnlySequence` without linearizing. Lazy iteration via `ReadOnlySequence.TryGet` — zero constructor allocation, no pre-extracted segment array. +`struct SequenceBinaryInput : IBinaryInputBase` — reads `ReadOnlySequence` without linearizing. Lazy iteration via `ReadOnlySequence.TryGet` — zero ctor alloc, no pre-extracted segment array. -The context's `_buffer` always points directly to the current segment's backing `byte[]` (zero-copy). Cross-boundary reads (value straddling segment boundary) copy only the affected bytes into a small `ArrayPool`-rented scratch buffer. After the scratch read, `_afterCrossBoundary` flag restores the context to the next segment's backing array. +Context `_buffer` points directly to current segment's backing `byte[]` (zero-copy). Cross-boundary reads (value straddling segments) copy only affected bytes into `ArrayPool`-rented scratch. After scratch read, `_afterCrossBoundary` flag restores context to next segment's backing array. -Typical overhead for 225KB payload with 4096-byte segments: ~224.5KB zero-copy, ~500 bytes scratch copy at ~55 boundaries. The scratch buffer is rented once (lazy, on first boundary) and reused across all boundaries. `Release()` returns it to `ArrayPool` after deserialization. +Typical overhead — 225KB payload, 4096-byte segments: ~224.5KB zero-copy + ~500 bytes scratch at ~55 boundaries. Scratch is rented once (lazy, first boundary) and reused; `Release()` returns it to `ArrayPool`. > Known issues: `AyCode.Core/AyCode.Core/docs/BINARY/BINARY_ISSUES.md` ## Configuration -Hub protocol settings are controlled via **`AcBinaryHubProtocolOptions`** (mutable class). Pass directly to the protocol constructor, or configure via DI in `Program.cs` with `services.Configure(opts => …)`. +Hub protocol settings via **`AcBinaryHubProtocolOptions`** (mutable class). Pass to ctor directly, or configure via DI in `Program.cs` (`services.Configure(opts => …)`). | Property | Default | Purpose | |----------|---------|---------| @@ -197,14 +197,14 @@ In `Bytes` and `Segment` mode, the standard `WriteMessage` path is used. ### WebAssembly compatibility -The send and receive paths handle WASM (`OperatingSystem.IsBrowser()`) asymmetrically — **send** is strictly bound to `_protocolMode`, **receive** adapts to the wire format and falls back to a synchronous path only when the platform cannot support the optimal strategy. +Send and receive paths handle WASM (`OperatingSystem.IsBrowser()`) asymmetrically — **send** strictly bound to `_protocolMode`; **receive** adapts to wire format, falls back synchronously when platform can't support the optimal strategy. - **Send path**: `AsyncSegment` is **not supported on WebAssembly**. `AcBinaryHubProtocolOptions.Validate()` throws `PlatformNotSupportedException` if `IsBrowser && ProtocolMode == AsyncSegment` (the `AsyncPipeWriterOutput.SyncAwaitFlush` sync-over-async pattern would block the single UI thread). WASM clients must use `Bytes` or `Segment`. - **Receive path**: works on WASM with **any** server-side mode (including `AsyncSegment` → chunked wire). `TryParseChunkData` detects the platform at runtime: - **Non-browser**: first `CHUNK_DATA` spawns a background `Task.Run` over a `SegmentBufferReader` (pipeline parallelism — serialize / network / deserialize overlap). `CHUNK_END` awaits the task's result. - **Browser**: the background task is skipped. Chunks accumulate in `SegmentBufferReader`; on `CHUNK_END` the buffer is `Complete()`d and the deserializer runs synchronously on the current thread. `SegmentBufferReaderInput.TryAdvanceSegment` sees `_completed=true` and never calls `ManualResetEventSlim.Wait()` (which throws `PlatformNotSupportedException` on WASM). -Consequence: a mixed topology (desktop server in `AsyncSegment`, WASM client in `Bytes`) works without any negotiation or protocol-name variation — the client converts the incoming chunked wire to its own synchronous processing model. +Consequence: mixed topology (desktop server `AsyncSegment` + WASM client `Bytes`) works without negotiation or protocol-name variation — client converts incoming chunked wire to its synchronous processing model. ## Registration in `Program.cs` @@ -273,7 +273,7 @@ services.AddSingleton(); // derived from AcSignalRClientBase ## Related ADRs -- [`adr/0001-acbinary-decorator-feature-stack-design.md`](../adr/0001-acbinary-decorator-feature-stack-design.md) — *AcBinaryHubProtocol optional feature stack — decorator-based composition design* (Status: Proposed). Umbrella ADR for the optional decorator-based feature stack (encryption, compression with `MinSize`, OpenTelemetry tracing, HMAC signing). Each NuGet-competitiveness TODO entry (`ACCORE-SBP-T-H7M5` / `N9F3` / `J5W8` / `B3K6` in `SIGNALR_BINARY_PROTOCOL_TODO.md`) resolves under this umbrella's architectural framework. Forthcoming leaf ADRs (0002-0005) will provide per-feature design + threat model. +- [`adr/0001-acbinary-decorator-feature-stack-design.md`](../adr/0001-acbinary-decorator-feature-stack-design.md) — *AcBinaryHubProtocol optional feature stack — decorator-based composition design* (Status: Proposed). Umbrella ADR for optional decorator-based feature stack (encryption, compression with `MinSize`, OpenTelemetry tracing, HMAC signing). NuGet-competitiveness TODO entries (`ACCORE-SBP-T-H7M5` / `N9F3` / `J5W8` / `B3K6` in `SIGNALR_BINARY_PROTOCOL_TODO.md`) resolve under this umbrella. Leaf ADRs (0002-0005) for per-feature design + threat model. - [`AyCode.Core/docs/adr/0003-acbinary-streaming-receive-architecture.md`](../../../docs/adr/0003-acbinary-streaming-receive-architecture.md) — *AcBinary streaming receive — AsyncPipeReaderInput unified primitive and transport-agnostic helpers* (Status: Proposed (2026-04-27)). Repo-level cross-cutting ADR establishing the receive-side architecture. Consolidates `SegmentBufferReader` + `SegmentBufferReaderInput` into a single `AsyncPipeReaderInput` class shared across SignalR (`TryParseChunkData` delegation in Step 6 / `ACCORE-SBP-T-G7T2`), NamedPipe IPC, and FileStream helpers. The unified AsyncSegment chunked wire format (`[INT32 length][200 CHUNK_START][201][UINT16 size][data][202 CHUNK_END]`) documented in this README's "Wire Format" / "BinaryProtocolMode" sections is preserved verbatim and is the transport-agnostic invariant the new ADR generalizes to NamedPipe + FileStream. **Source:** `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs` (base), `AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs` (consumer logic), `AyCode.Services/SignalRs/BinaryProtocolMode.cs` (enum), `AyCode.Services/SignalRs/AcBinaryHubProtocolOptions.cs` (options), `AyCode.Services/SignalRs/AcSignalRProtocolExtensions.cs` (DI extensions) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 39d43ca..94f413a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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(config.GetSection("AyCode:Xxx")); services.AddAcXxxFactory(); ``` -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` 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` 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 { ... } class User : AcUser { ... } ``` -This allows the framework to define relationships without knowing concrete types. +Framework defines relationships without knowing concrete types. diff --git a/docs/AUTH/README.md b/docs/AUTH/README.md index 55971eb..da059c1 100644 --- a/docs/AUTH/README.md +++ b/docs/AUTH/README.md @@ -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 diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index 8c6f492..4f180b1 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -6,14 +6,14 @@ - **Interfaces:** Standard `I` prefix + `Ac` (e.g., `IAcDbContextBase`, `IAcUserDbSetBase`). - **Extensions:** `{Domain}Extensions.cs` (e.g., `StringExtensions`, `CollectionExtensions`, `AcDbSessionExtension`). - **Test bases:** `Ac{Domain}TestBase` or `AcBase_{TestName}` for inherited test methods. -- **Folder names — plural** (workspace-wide): all source folder names use plural form to avoid type-vs-folder namespace collisions (e.g. `Serializers/AcBinarySerializer.cs`, NOT `Serializer/AcBinarySerializer.cs`). The plural form gives the namespace its own identity, separate from any single type within it. -- **English-only identifiers** (workspace-wide): all type names, member names, namespaces, file names, and folder names use English. Native-language identifiers (Hungarian or otherwise) are not allowed even for product/consumer-specific types — keep code readable for any LLM or human collaborator regardless of native language. +- **Folder names — plural** (workspace-wide): source folder names plural to avoid type-vs-folder namespace collisions (`Serializers/AcBinarySerializer.cs`, NOT `Serializer/`). Namespace gets its own identity, separate from any single type. +- **English-only identifiers** (workspace-wide): all identifiers (types, members, namespaces, files, folders) use English. Native-language names (Hungarian or otherwise) forbidden even in product/consumer code — readability for LLM/human collaborators. > **Workspace-wide note:** the framework-only **class prefix mandate** (`Ac` for AyCode.*, `Mg` for Mango.Nop.*; product/consumer repos un-prefixed) is an architectural rule — see `ARCHITECTURE.md#class-prefix--framework-only-mandate`. The first bullet above is the AyCode.Core-specific instance of that rule. ## Patterns -- **Extension methods over instance methods** for CRUD operations. Keeps interfaces clean, implementations composable. +- **Extension methods over instance methods** for CRUD — clean interfaces, composable impls. - **Session/Transaction pattern** in DataLayers: `Session()` for reads, `Transaction()` for writes. Both are mutex-protected. - **Generic interface hierarchy** for entities: Interface → Abstract Entity → Concrete (in consuming project). - **Partial classes** for large serializers (AcBinarySerializer, AcToonSerializer split across 10+ files). @@ -37,7 +37,7 @@ See `AyCode.Services/docs/SIGNALR/README.md` for full architecture documentation ### ⚠️ Temporary: JSON-in-Binary Request Parameters -Client→server request parameters currently use a JSON-inside-Binary envelope — a cross-cutting tech debt planned for migration to pure Binary. Canonical entry: `AyCode.Core/AyCode.Core/docs/XCUT/XCUT_ISSUES.md#accore-xcut-i-x8q1`. Cross-refs: `BINARY_ISSUES.md#accore-xcut-i-x8q1` (serializer side) and `SIGNALR_ISSUES.md#accore-xcut-i-x8q1` (transport side). Migration is tracked in `BINARY_TODO.md#accore-bin-t-s8p4`. Do NOT attempt as a side-effect of unrelated work — requires coordinated client+server+consuming-project changes. +Client→server request parameters use a JSON-inside-Binary envelope — cross-cutting tech debt, migration to pure Binary planned. Canonical: `AyCode.Core/AyCode.Core/docs/XCUT/XCUT_ISSUES.md#accore-xcut-i-x8q1`. Cross-refs: `BINARY_ISSUES.md` + `SIGNALR_ISSUES.md` (same ID). Migration: `BINARY_TODO.md#accore-bin-t-s8p4`. Do NOT attempt as a side-effect — requires coordinated client+server+consuming-project changes. ## Testing @@ -48,7 +48,7 @@ Client→server request parameters currently use a JSON-inside-Binary envelope ## Framework-First Placement -Every new type/feature requires asking: *generic or consumer-specific?* +Every new type — *generic or consumer-specific?* | Trait | Verdict | |-------|---------| @@ -57,7 +57,7 @@ Every new type/feature requires asking: *generic or consumer-specific?* | Abstract/virtual hooks for consumer customization | **ACCEPT** in framework | | Requires a specific concrete type from a consumer | **REDESIGN** — use generic/options/extension pattern | -Before committing any type to this framework: +Pre-commit check: - No consumer-name string in any identifier, namespace, docstring, or doc - No hardcoded consumer-specific values - Extension methods / base classes / options classes cover the N-consumer use case diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 1a23a82..fcd0b18 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -1,6 +1,6 @@ # Glossary -Core terminology for the AyCode framework. Read this before working on unfamiliar areas. +Core terminology. Read before working on unfamiliar areas. ## Core Abstractions @@ -15,11 +15,11 @@ Core terminology for the AyCode framework. Read this before working on unfamilia | Term | Definition | |---|---| -| **AcBinary** | High-performance binary serializer. Two-phase: scan (collect metadata) then serialize. Supports reference tracking, string interning, ID-based deduplication. | -| **Toon** | Token-Oriented Object Notation. LLM-optimized format with @meta (schema) and @data (values) sections. Designed for maximum LLM comprehension accuracy. | -| **AcJson** | Newtonsoft.Json wrapper with $id/$ref reference handling, IId-based resolution, and chain deserialization API. | -| **Chain API** | Fluent deserialization: `CreateDeserializeChain().ThenDeserialize()...Execute()`. Resolves cross-references across multiple types. | -| **String Interning** | Binary serializer deduplicates repeated strings. Controlled via `[AcStringIntern]` attribute or `StringInterningMode`. See `AyCode.Core/AyCode.Core/docs/BINARY/BINARY_FEATURES.md`. | +| **AcBinary** | High-perf binary serializer. Two-phase: scan (metadata) → serialize. Reference tracking, string interning, ID-based dedup. | +| **Toon** | Token-Oriented Object Notation. LLM-optimized: @meta (schema) + @data (values), maximized for LLM comprehension. | +| **AcJson** | Newtonsoft.Json wrapper: $id/$ref handling, IId-based resolution, chain deserialization API. | +| **Chain API** | Fluent deserialization: `CreateDeserializeChain().ThenDeserialize()...Execute()`. Resolves cross-refs across multiple types. | +| **String Interning** | Binary serializer dedup of repeated strings. Controlled via `[AcStringIntern]` or `StringInterningMode`. See `AyCode.Core/AyCode.Core/docs/BINARY/BINARY_FEATURES.md`. | ## Binary Wire Format @@ -32,7 +32,7 @@ For full specification see `AyCode.Core/AyCode.Core/docs/BINARY/BINARY_FORMAT.md | **FixStr** | Compact string marker (103–134). Encodes type + length in one byte for ASCII strings ≤31 bytes. | | **TinyInt** | Compact integer marker (192–255). Encodes small integers (−16 to 47) in a single byte. | | **VarInt / VarUInt** | Variable-length integer encoding. LEB128 for unsigned, ZigZag + LEB128 for signed. | -| **SequenceBinaryInput** | `struct : IBinaryInputBase` for reading from `ReadOnlySequence` (multi-segment pipe data). Lazy iteration via `TryGet` — zero constructor allocation. Context `_buffer` points directly to segment backing `byte[]` (zero-copy). Cross-boundary values use `ArrayPool`-rented scratch buffer (rent once, reuse, `Release()` returns). N-segment loop handles values spanning any number of segments. | +| **SequenceBinaryInput** | `struct : IBinaryInputBase` for reading multi-segment pipe data (`ReadOnlySequence`). Lazy iteration, zero-alloc ctor, `ArrayPool` scratch for cross-segment reads. Full spec: `AyCode.Core/AyCode.Core/docs/BINARY/BINARY_FORMAT.md`. | | **ArrayBinaryInput** | `struct : IBinaryInputBase` for reading from contiguous `byte[]`. Zero-copy when pipe is single-segment. Default fast-path for deserialization. | | **HeaderFlags** | Byte at stream position 1 encoding serialization options: metadata, reference handling mode, cache count presence. Base `0x90`. | | **Two-Phase Serialization** | Scan pass detects multi-referenced objects, serialize pass writes output using reference table. Required for `ReferenceHandling.All`. | @@ -72,14 +72,14 @@ For full architecture see `AyCode.Services/docs/SIGNALR/README.md`. | Term | Definition | |---|---| -| **OnReceiveMessage** | The single SignalR method for all communication. Signature: `(int messageTag, int? requestId, SignalParams signalParams, object data)`. Metadata and payload are separate hub arguments. `data` is typed object (protocol eagerly deserializes via `SignalDataType`), raw `byte[]` (IsRawBytesData or byte[] fast-path), or null. `SignalParams.Parameters` carries packed method params as `byte[]`. Both are independent and nullable in any direction. | -| **SignalParams** | Metadata sent alongside message payload as separate hub argument. Contains `Status`, `DataSerializerType`, `Parameters` (`byte[]?` — packed `byte[][]` as single blob), `SignalDataType` (`string?` — response type for eager deserialization), `IsRawBytesData` (`bool` — return raw bytes without deserialization). Typed access via `SetParameterValues(object[])` / `GetParameterValues(ParameterInfo[])` — PostDataJson pattern. `[AcBinarySerializable]`. Never null — only fields inside are nullable. | +| **OnReceiveMessage** | Single SignalR method for all communication. Tag-based routing via `int messageTag`; metadata + payload as separate hub args. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. | +| **SignalParams** | Metadata accompanying each message (separate hub arg). Carries Status, response type, packed parameters, raw-bytes flag. `[AcBinarySerializable]`. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. | | **Message Tag** | Integer identifier mapping to a method via `[SignalR(tag)]` or `[SignalRSendToClient(tag)]` attributes. | | **DynamicMethodRegistry** | Resolves message tags to `MethodInfo` at runtime. Static `ConcurrentDictionary` cache with lazy scan on miss. | | **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`. | -| **AcBinaryHubProtocol** | Unsealed base `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"` (configurable). Options-based ctor: `new AcBinaryHubProtocol(AcBinaryHubProtocolOptions)`. Write: `BufferWriterBinaryOutput` / `AsyncPipeWriterOutput` zero-copy to pipe. Read: `ArrayBinaryInput` via `GetArgBytes` (zero-copy single-seg / pool-rent multi-seg) for non-chunked; chunked receive via `SegmentBufferReader` + `SegmentBufferReaderInput` with platform-aware fallback. | -| **AyCodeBinaryHubProtocol** | Consumer-specific derived protocol (per-message header with `DataFlags`, `IsRawBytesData`, type resolution). Registered via `services.AddSignalR().AddAcBinaryProtocol(...)` on the server and `hubBuilder.AddAcBinaryProtocol(...)` on the client. | -| **AcBinaryHubProtocolOptions** | Mutable config class for protocol registration. Properties: `SerializerOptions`, `ProtocolMode`, `BufferSize`, `WaitForFlush`, `FlushTimeout`, `Name`, `Logger`. `Validate()` enforces invariants (incl. WASM + AsyncSegment block). `Clone()` for DI `IOptions` safety. | +| **AcBinaryHubProtocol** | Unsealed base `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. Full spec (write/read paths, chunked framing): `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md`. | +| **AyCodeBinaryHubProtocol** | Consumer-derived protocol (per-message header with type resolution, `IsRawBytesData`). Registered via `AddAcBinaryProtocol(...)` DI extension on both server and client. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. | +| **AcBinaryHubProtocolOptions** | Mutable config for protocol registration (SerializerOptions, ProtocolMode, BufferSize, FlushTimeout, etc.). `Validate()` + `Clone()` for DI safety. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. | | **AcSignalRProtocolExtensions** | DI extension class: `AddAcBinaryProtocol(ISignalRServerBuilder, Action?)` for server, `AddAcBinaryProtocol(IHubConnectionBuilder, Action<...>?)` for client. DI `IOptions` + inline-configure override chain. | | **SignalResponseDataMessage** | Internal DTO for client callback routing and stream wire format (not serialized as envelope on wire). `RawResponseData` is `object?` (typed object or byte[]). `GetResponseData()` performs direct cast. | | **SignalPostJsonDataMessage** | OBSOLETE — still exists but marked `[Obsolete]`. Legacy: serialized params to JSON inside Binary envelope. | diff --git a/docs/README.md b/docs/README.md index 6c43db4..99685bf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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.