From 73d81ea580a9198d0c6c99044165e711b27937ff Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 11 May 2026 13:28:43 +0200 Subject: [PATCH] [LOADED_DOCS: 7 files, no new loads] AcBinary: add framing doc, buffer growth fixes, doc updates - Added `BINARY_WHYUSE.md` for architectural framing and value proposition - Updated `BINARY_FEATURES.md` and `README.md` to reference and prioritize the new doc - Documented AsyncPipeWriterOutput chunk-size limitation and workarounds in `BINARY_ASYNCPIPE_ISSUES.md` - Refactored buffer growth logic in `AcBinarySerializer.BinarySerializationContext.cs` to validate capacity after grow and throw clear exceptions on under-provisioning; removed dead method - Fixed chunk size alignment bug in `AsyncPipeWriterOutput.cs` to prevent buffer under-provisioning - Added `AYCODE_NATIVEAOT` build config support in `Program.cs` - Improved documentation clarity and error diagnostics for streaming/buffered serialization edge cases --- AyCode.Core.Serializers.Console/Program.cs | 4 +- ...rySerializer.BinarySerializationContext.cs | 41 +++++++----- .../Binaries/AsyncPipeWriterOutput.cs | 17 ++++- .../docs/BINARY/BINARY_ASYNCPIPE_ISSUES.md | 42 +++++++++++++ AyCode.Core/docs/BINARY/BINARY_FEATURES.md | 2 + AyCode.Core/docs/BINARY/BINARY_TODO.md | 2 + AyCode.Core/docs/BINARY/BINARY_WHYUSE.md | 62 +++++++++++++++++++ AyCode.Core/docs/BINARY/README.md | 3 +- 8 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 AyCode.Core/docs/BINARY/BINARY_WHYUSE.md diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 1f4996f..103679b 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -34,7 +34,9 @@ public static class Program { private const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark"; -#if DEBUG +#if AYCODE_NATIVEAOT + private const string BuildConfiguration = "NativeAOT"; +#elif DEBUG private const string BuildConfiguration = "Debug"; #else private const string BuildConfiguration = "Release"; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 34834e8..3a8fea7 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -364,18 +364,25 @@ public static partial class AcBinarySerializer [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureCapacity(int additionalBytes) { - if (_position + additionalBytes > _bufferEnd) - Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes); + if (_position + additionalBytes > _bufferEnd) GrowAndValidate(additionalBytes); } /// - /// Ensures the buffer has enough space for the specified number of bytes. - /// Called before property writes to avoid mid-object Grow() calls. + /// Slow path for : invokes Output.Grow and revalidates that + /// the request was actually satisfied. A chunk-limited output (e.g. AsyncPipeWriterOutput + /// on a single-value write > MaxChunkSize data bytes) may return WITHOUT growing the + /// active buffer to the requested size — silent under-provisioning would cause downstream + /// ArgumentOutOfRangeException in AsSpan/CopyTo at corrupt offsets. + /// NoInlining keeps the hot path of tiny and inline-friendly. + /// Related limit: see BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ReserveCapacity(int bytes) + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndValidate(int additionalBytes) { - EnsureCapacity(bytes); + Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes); + + var available = _bufferEnd - _position; + if (available < additionalBytes) ThrowGrowFailedToSatisfy(additionalBytes, available); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -386,13 +393,6 @@ public static partial class AcBinarySerializer _buffer[_position++] = value; } - /// Writes a single byte without capacity check. Caller must ensure buffer space. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteByteUnsafe(byte value) - { - _buffer[_position++] = value; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteTwoBytes(byte b1, byte b2) { @@ -1004,6 +1004,19 @@ public static partial class AcBinarySerializer $"This limit is dictated by the writer's worst-case 'charLength * 4' UTF-8 byte allocation; " + $"larger inputs would silently overflow int arithmetic."); + /// + /// Throw helper for : invoked when Output.Grow returns + /// without satisfying the requested capacity (chunk-limited output, single value larger than + /// the per-chunk data capacity). Marked NoInlining so the throw-site stays out of the + /// inlined caller body. See BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9 for fix direction. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowGrowFailedToSatisfy(int requested, int available) => + throw new InvalidOperationException( + $"Output.Grow did not satisfy the requested capacity: {requested} bytes requested, only {available} bytes available after Grow. " + + $"This typically indicates a chunk-limited output (e.g. AsyncPipeWriterOutput) with a single serialized value exceeding the per-chunk data capacity. " + + $"See BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9."); + #endregion #region Bulk Array Writes — inline diff --git a/AyCode.Core/Serializers/Binaries/AsyncPipeWriterOutput.cs b/AyCode.Core/Serializers/Binaries/AsyncPipeWriterOutput.cs index 302e883..7e65f71 100644 --- a/AyCode.Core/Serializers/Binaries/AsyncPipeWriterOutput.cs +++ b/AyCode.Core/Serializers/Binaries/AsyncPipeWriterOutput.cs @@ -284,7 +284,22 @@ public struct AsyncPipeWriterOutput : IBinaryOutputBase } // Acquire new chunk with header reservation (common to both paths). - AcquireChunk(Math.Max(needed, _chunkSize), out buffer, out position, out bufferEnd); + // + // Unit alignment: `needed` is the data-byte count requested by the caller (via EnsureCapacity); + // `_chunkSize` is the chunk-on-wire total size (header + data) — same units AcquireChunk's + // `requestSize` parameter expects. Convert `needed` to the chunk-on-wire scale (`+ headerOffset`) + // before the Max() so apples-to-apples. AcquireChunk then subtracts `headerOffset` internally to + // size the data region. + // + // Without this alignment, a caller request of exactly `_chunkSize - headerOffset` data bytes + // would yield a chunk whose data region is `headerOffset` bytes SHORT of the request — every + // EnsureCapacity for a value larger than the chunk-data capacity would silently under-provision, + // surfacing later as `ArgumentOutOfRangeException` in `AsSpan`/`CopyTo` at corrupt offsets. + // The sanity guard in `BinarySerializationContext.EnsureCapacity` (post-Grow revalidation + + // `ThrowGrowFailedToSatisfy`) catches the remaining > MaxChunkSize case explicitly — see + // `BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9`. + var headerOffset = _multiMessage ? HeaderSize : 0; + AcquireChunk(Math.Max(needed + headerOffset, _chunkSize), out buffer, out position, out bufferEnd); _currentChunkStart = position; } diff --git a/AyCode.Core/docs/BINARY/BINARY_ASYNCPIPE_ISSUES.md b/AyCode.Core/docs/BINARY/BINARY_ASYNCPIPE_ISSUES.md index 8cfd64f..bc7a9ad 100644 --- a/AyCode.Core/docs/BINARY/BINARY_ASYNCPIPE_ISSUES.md +++ b/AyCode.Core/docs/BINARY/BINARY_ASYNCPIPE_ISSUES.md @@ -74,6 +74,48 @@ The current class summary implicitly assumes one of these patterns; the public A **Possible documentation direction** tracked in [`BINARY_ASYNCPIPE_TODO.md#accore-bin-t-s5n2`](BINARY_ASYNCPIPE_TODO.md#accore-bin-t-s5n2-pattern-catalogue-in-the-public-class-summary). +### ACCORE-BIN-I-B7K9: AsyncPipeWriterOutput single-value writes > MaxChunkSize data bytes not supported + +**Status:** Open +**Affects:** `AsyncPipeWriterOutput.AcquireChunk` (and indirectly any `BinarySerializationContext.EnsureCapacity` call where the request exceeds per-chunk data capacity) +**Reach:** any single serialized value whose worst-case byte-count exceeds `MaxChunkSize - HeaderSize` (~65 532 byte in framed mode). Typical trigger: a `string` property with > ~16 383 chars (writer pre-reserves `4 * charLength + 9` bytes for worst-case UTF-8 encoding); also `byte[]` or other values larger than the chunk-data capacity. + +**Symptom:** `BinarySerializationContext.EnsureCapacity` now surfaces a clear `InvalidOperationException` ("Output.Grow did not satisfy the requested capacity: N bytes requested, only M bytes available after Grow") instead of the previous downstream `ArgumentOutOfRangeException` from `_buffer.AsSpan(start, length)` / `CopyTo` at corrupt offsets. The exception is intentional — surfaces the chunk-limit-vs-request mismatch at the precise call site, no silent wire corruption. + +**Root cause:** The multi-message wire format uses `[201][UINT16 size][data]...[202]` framing. The `UINT16` cap (`MaxChunkSize = 65 535` total = 65 532 data bytes in framed mode) is a wire-format-level constraint. Currently `AcquireChunk` clamps any single contiguous request to this cap; values requiring more contiguous space in one Grow-event cannot be served by chunk-pool memory. + +**Why intentional limit (UINT16 size field):** The wire-format `UINT16 size` field is a deliberate compactness trade-off — 1-byte overhead per chunk header is minimal vs UINT32 (would cost +2 bytes per chunk overhead, measurable on small-chunk transports). The cap is intrinsic to the framing format; the FIX is at the producer side (transparent split-on-commit), not the wire format. + +**Workarounds (current — pre-fix):** +- **Avoid single string values > ~16 383 chars** in serialized graphs (4-byte/char worst-case keeps `4 * charLength + 9 ≤ 65 532`). Real workloads with mostly-ASCII content can practically reach ~65K chars before hitting the cap, but the safe-by-construction limit is 16K. +- **External chunking**: pre-split very large strings (file contents, blobs) into multiple smaller properties or use a dedicated `byte[]` array property with consumer-side chunking. +- **Use a non-Pipe transport for the affected message types** — `byte[]` / `BufferWriter` output mode has no chunk-cap (`ArrayBinaryOutput.Grow` / `BufferWriterBinaryOutput.Grow` size to whatever is needed). + +**Related semantic fix in same session (off-by-headerOffset):** `AsyncPipeWriterOutput.Grow` previously passed `Math.Max(needed, _chunkSize)` to `AcquireChunk`, mixing units (`needed` is data-bytes, `_chunkSize` is chunk-on-wire = header + data). `AcquireChunk` subtracts `headerOffset` from the request internally to size the data region — so every Grow request was short by `headerOffset` (3) bytes. Fixed: `Math.Max(needed + headerOffset, _chunkSize)` aligns both terms to chunk-on-wire scale. This closes the off-by-3 chunk-shortage band (1021..16383 char range was affected even though < cap); the `> MaxChunkSize` band remains a separate concern tracked here. + +**Possible fix direction (transparent split-on-commit):** + +Implement an owned-buffer fallback inside `AsyncPipeWriterOutput`: + +1. `AcquireChunk` detects `requestSize > MaxChunkSize - headerOffset` → force `_ownedBuffer = ArrayPool.Shared.Rent(requestSize)` path (instead of `_pipeWriter.GetMemory`). Returns the contiguous owned buffer; `_hasOwnedBuffer = true`. +2. Hot-path writers (`WriteStringWithDispatch`, `WriteByteArray`, etc.) write into the owned buffer unchanged — no API contract change, no caller-side awareness. +3. `CommitCurrentChunk` (or `Flush`) on a `_hasOwnedBuffer = true` state splits the owned buffer into `MaxChunkSize - headerOffset`-sized data chunks, each framed with its own `[201][UINT16 size]` header into the `_pipeWriter`'s memory, and emits them sequentially. The `[202]` end marker remains owned by `Flush` (only DATA chunks are split — the message-end marker is per-message, not per-chunk). +4. Receiver side (`AsyncPipeReaderInput.Feed` framing-state-machine) is **not affected** — the wire still arrives as a stream of `[201]`-framed data chunks; the sliding-window buffer reassembles bytes for the deserializer regardless of how the producer split them. + +Allocation profile: 1× ArrayPool rent + 1× return per `>MaxChunkSize` event. ArrayPool slab reuse keeps per-event overhead near-zero on repeated occurrences. Hot path (`≤ MaxChunkSize` values) entirely unchanged. + +**Acceptance** (for the future TODO entry): +- New test: serialize an entity with a string property whose UTF-8 byte length > 65 532 bytes over `AsyncPipeWriterOutput`; round-trip equality holds. +- Test: serialize a `byte[]` property > 65 532 bytes; round-trip equality holds. +- Test: mixed message — small + large + small string properties on the same message → all wire frames valid, deserializer reconstructs each in order. +- Wire format unchanged — receiver code untouched. +- Allocation profile: 1 rent + 1 return per >cap event; no allocation regression on ≤cap path. + +**Cross-references:** +- Sanity guard added: `BinarySerializationContext.EnsureCapacity` → `GrowAndValidate` → `ThrowGrowFailedToSatisfy` (clear diagnostic instead of silent under-provisioning). +- Off-by-headerOffset semantic fix: `AsyncPipeWriterOutput.Grow` line ~287 (see "Related semantic fix" above). +- Wire format: `BINARY_FORMAT.md` (framing constants). + ## Streaming protocol — resolved issues ### ACCORE-BIN-I-H4G2: Chunk-on-wire size = `chunkSize + HeaderSize` caused page-fragmentation diff --git a/AyCode.Core/docs/BINARY/BINARY_FEATURES.md b/AyCode.Core/docs/BINARY/BINARY_FEATURES.md index e50f01f..10cd08f 100644 --- a/AyCode.Core/docs/BINARY/BINARY_FEATURES.md +++ b/AyCode.Core/docs/BINARY/BINARY_FEATURES.md @@ -2,6 +2,8 @@ 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`. +> **Architectural framing — why use AcBinary at all?** See [`BINARY_WHYUSE.md`](BINARY_WHYUSE.md) for the category positioning vs wire-only serializers (Protobuf / MessagePack / MemoryPack), the three-pillar value proposition (graph integrity + bandwidth + streaming), real-world WASM SignalR reference numbers, fit/not-fit decision lists, and the framing for how to read AcBinary's benchmark numbers. + ## Optimization Policy (LLM) AcBinary is a **general-purpose serializer**, not a benchmark-only implementation. diff --git a/AyCode.Core/docs/BINARY/BINARY_TODO.md b/AyCode.Core/docs/BINARY/BINARY_TODO.md index 2a6dbcd..8f2aec1 100644 --- a/AyCode.Core/docs/BINARY/BINARY_TODO.md +++ b/AyCode.Core/docs/BINARY/BINARY_TODO.md @@ -1352,6 +1352,8 @@ A V4N4 audit **konklúziója** változatlan érvényes (constant-fold OK, reader **Re-evaluable as of 2026-05-07 per `ACCORE-BIN-T-D9X3`** — bench stabilization removes the noise-floor that made the original signal unmeasurable; retest before any code change. **Obsoleted (2026-05-08) by `ACCORE-BIN-T-K7M3`** — the writer hot path no longer calls the custom `EncodeUtf8SinglePass` at all (`WriteStringWithDispatch` was switched to `Utf8.FromUtf16` BCL). The "AOT method-split / inlining audit" target (`Utf8Transcoder` body method-size in NativeAOT inline budget) is moot — the BCL `Utf8.FromUtf16` is a single static method with its own AOT-friendly inline footprint, and the audit's hypothesis space (Vector256 `IsSupported` constant-fold, lambda delegate cache) was correct for the prior code but no longer applies. The V4N4 disasm methodology remains a **valid technique** for future investigations of generic specialization / inline failures, but the specific hot-path target it analyzed is gone. + +## ACCORE-BIN-T-J5L9: Remove dead `WriteFixStrDirect` / `WriteStringUtf8Internal` (audit-surfaced uncalled methods) **Priority:** P3 · **Type:** Refactor / hygiene · **Status:** Closed (2026-05-06) · **Related:** `BinarySerializationContext.cs` V4N3 audit surfaced two methods with no callers in the entire workspace: diff --git a/AyCode.Core/docs/BINARY/BINARY_WHYUSE.md b/AyCode.Core/docs/BINARY/BINARY_WHYUSE.md new file mode 100644 index 0000000..c242b18 --- /dev/null +++ b/AyCode.Core/docs/BINARY/BINARY_WHYUSE.md @@ -0,0 +1,62 @@ +# Why AcBinary? + +Architectural framing: where AcBinary fits in the serializer landscape, what value it adds over wire-only serializers, and how to read its benchmark numbers in that context. + +> Companion docs: features detail in [`BINARY_FEATURES.md`](BINARY_FEATURES.md) · wire format in [`BINARY_FORMAT.md`](BINARY_FORMAT.md) · options/presets in [`BINARY_OPTIONS.md`](BINARY_OPTIONS.md) · streaming I/O in [`BINARY_ASYNCPIPE_ISSUES.md`](BINARY_ASYNCPIPE_ISSUES.md) + [`BINARY_ASYNCPIPE_TODO.md`](BINARY_ASYNCPIPE_TODO.md). + +## Category + +AcBinary serves a **different category** than wire-only serializers (Protobuf, MessagePack, MemoryPack). It is a **graph-aware** serializer with referential integrity preserved on the wire via IdTracking + StringInterning, and on the receive side via a populate-merge deserialization path that reuses existing reference identity. The primary value is **correctness and developer-ergonomics in stateful live-data scenarios** (Blazor / MAUI / WPF binding, SignalR streaming, server-push reconciliation) — **not** raw single-shot throughput on flat DTO RPC. + +## Three-pillar value proposition + +### 1. Graph integrity → populate-merge correctness in bound UIs + +On a live-data UI, an incoming server update must merge into the in-memory graph **without** breaking binding references, child-list identity, or duplicating shared sub-objects (`Product`, `Customer`, `Partner`, etc. — entities referenced from many parent rows). Wire-only serializers always reconstruct a fresh tree per call → the consumer hand-codes the merge / rebinding logic, which is fragile and verbose. AcBinary's `IId`-keyed reference tracking preserves identity end-to-end: shared sub-objects deduplicate on the wire AND on the client, existing references survive the round-trip, bindings stay valid, change-tracking continues uninterrupted. This is the **central value** for Blazor components, `IList`-bound MAUI grids, and SignalR datasource subscribers. + +### 2. Bandwidth → reference + string deduplication on the wire + +On real production graphs (many `Order → OrderItem → Product → Customer → Partner → GenericAttribute` references sharing the same back-end entities), IdTracking and StringInterning emit each unique object/string **once** as the full body, then 1-2 bytes per subsequent reference. Wire-only serializers re-emit the full object body every time. Bench numbers (at **20% `IId`-ref-rate** in the test fixture): + +- **Latin1Long charset**: -6.9% arith / -7.4% geo wire size vs MemoryPack +- **Latin1FixAscii charset**: -18.2% arith / -21.3% geo wire size vs MemoryPack + +Production graphs with higher ref-rate (the typical case — many rows pointing at the same `Product`/`Customer`) see significantly larger savings; see [How to read AcBinary benchmarks](#how-to-read-acbinary-benchmarks) below. + +### 3. Streaming → memory pressure mitigation on memory-constrained hosts + +`AsyncPipeReaderInput` + `AsyncPipeWriterOutput` deliver chunked I/O with **peak memory ≈ chunk-size**, not full payload size. On WASM (Mono / NativeAOT-LLVM), a 10 MB monolithic `byte[]` allocation triggers GC pressure or OOM under concurrent task load; chunked delivery keeps the working set bounded. Combined with the wire-size win, decode CPU drops proportionally — **fewer bytes on the wire → fewer varint + UTF-8 decode operations** during deserialization. SignalR's `AcBinaryHubProtocol` consumes via `AsyncPipeReaderInput` directly, no monolithic buffer materialization. + +## Real-world reference: WASM SignalR receive + +Measured production payload: ~10 MB / ~7900 orders (full graph `Order → OrderItem → Product → Customer → Partner → GenericAttribute`), 4 SignalR messages, Blazor WASM client. + +- **Before AcBinary (MemoryPack baseline)**: ~8 seconds Deser for ~4K records / ~20 MB payload — the MemoryPack output was nearly 2× larger because the same `Product`/`Customer` entities re-serialized per Order. JSON baseline was ~30 seconds. +- **After AcBinary**: ~470 ms Deser for ~8K records / ~10 MB payload — wire compaction from graph deduplication + chunked-pipe streaming + decode-CPU reduction stack multiplicatively. + +The speed-up is **not** primarily "AcBinary is faster than MemoryPack on the same bytes." It is **"AcBinary emits ~50% fewer bytes for the same graph, then decodes them with bounded memory pressure"**. The feature stack is the win — single-cell bench Ser/Deser ratios alone do not capture it. + +## When AcBinary fits + +- Live-data UIs requiring graph-merge into a bound client-side model (Blazor / MAUI / WPF / WinForms with two-way binding, SignalR datasource subscribers) +- SignalR / WebSocket / IPC transports with repeated entity references across messages +- Server-push reconciliation flows where ID-keyed identity must survive the wire round-trip +- Memory-bounded clients (WASM, mobile, embedded) receiving large object graphs +- Workloads where **wire-size + decode-CPU + memory-peak** matter together, not just single-message raw Ser throughput + +## When AcBinary is NOT the right fit + +- Single-shot DTO RPC with no entity reuse (e.g., `(int, int) → int` calculator endpoints) — wire-only serializers have less per-message overhead on flat payloads +- Schema-evolving public contracts where wire-format stability across language ecosystems is the primary requirement → Protobuf is the established choice +- Cross-platform clients on big-endian hosts → currently unsupported on the wire (see [`BINARY_ISSUES.md#accore-bin-i-e4n9`](BINARY_ISSUES.md#accore-bin-i-e4n9-wire-format-is-host-native-endian-not-canonical-little-endian)) +- Append-only log formats where each record is independent — IdTracking's scan-phase overhead is wasted when there are no shared references to deduplicate + +## How to read AcBinary benchmarks + +`Console.FullBenchmark` test cells use **20% `IId`-ref-rate** test fixtures — flat enough to compare against wire-only serializers like MemoryPack on the same data shape. The cells **deliberately undermeasure** the feature wins: + +- Production graphs typically have 50-90% `IId`-ref-rate (many rows → same `Product` / `Customer`); IdTracking's wire-size win on those graphs is **multiples larger** than the bench numbers show. +- `Repeated Strings` is the only bench cell stress-testing StringInterning. Other cells have low string-repetition, so the interning win there is bounded by the fixture. +- The bench does NOT cover the `AsyncPipe` path (only `Byte[]` and `BufferWriter` I/O modes). The streaming-memory advantage doesn't surface in the bench numbers. + +Bench wins on the current fixture (-6 to -10% RT, -7 to -18% size) should be read as a **lower bound** for AcBinary's production advantage on graph-shaped, ref-heavy workloads. The matching position against MemoryPack on flat-payload Ser/Deser is **intentional** — AcBinary should not pay a tax for its features on workloads that don't benefit from them. The cases where the features pay back are workloads the bench does not measure directly; the WASM reference numbers above are the production-scale signal. diff --git a/AyCode.Core/docs/BINARY/README.md b/AyCode.Core/docs/BINARY/README.md index aa7b22b..f97678c 100644 --- a/AyCode.Core/docs/BINARY/README.md +++ b/AyCode.Core/docs/BINARY/README.md @@ -4,6 +4,7 @@ AcBinary serialization system. Primary goal: **speed** (two-phase scan+serialize ## Files in this folder +- [`BINARY_WHYUSE.md`](BINARY_WHYUSE.md) — Architectural framing: why AcBinary exists (category vs wire-only serializers, three-pillar value proposition, fit/not-fit decision lists, benchmark reading guide) - [`BINARY_FEATURES.md`](BINARY_FEATURES.md) — High-level features and capabilities - [`BINARY_FORMAT.md`](BINARY_FORMAT.md) — Wire format specification - [`BINARY_OPTIONS.md`](BINARY_OPTIONS.md) — Configuration options (`AcBinaryOptions`) @@ -17,7 +18,7 @@ AcBinary serialization system. Primary goal: **speed** (two-phase scan+serialize ## Start here -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. +Start with [`BINARY_WHYUSE.md`](BINARY_WHYUSE.md) (architectural framing — why this serializer over the alternatives), then [`BINARY_FEATURES.md`](BINARY_FEATURES.md) (feature overview), then [`BINARY_FORMAT.md`](BINARY_FORMAT.md) (wire-level). [`BINARY_SGEN.md`](BINARY_SGEN.md) covers build-time code-gen integration. ## Cross-references