AyCode.Core/AyCode.Core/docs/BINARY/BINARY_WHYUSE.md

6.9 KiB
Raw Blame History

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 · wire format in BINARY_FORMAT.md · options/presets in BINARY_OPTIONS.md · streaming I/O in BINARY_ASYNCPIPE_ISSUES.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<T>-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 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)
  • 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.