[LOADED_DOCS: 3 files, no new loads]

Add docs for AcBinary MVC formatters and pipeline updates

Comprehensive documentation for new ASP.NET Core MVC formatters supporting AcBinary, including registration, media type, request/response flow, error handling, and future plans. Updated project and topic docs to reference MVC formatters and folder structure. Added performance planning entry for StreamPipeWriter congestion fallback. Expanded markerless schema lane rationale and updated architecture docs to reflect MVC formatter integration. Improved navigation and layering documentation.
This commit is contained in:
Loretta 2026-05-05 06:55:32 +02:00
parent 7d9cf10a6e
commit 58f7a1c286
8 changed files with 151 additions and 3 deletions

File diff suppressed because one or more lines are too long

View File

@ -878,12 +878,31 @@ The 1.04+ ratio observed in JIT-mode benchmarks (12-50-43, 13-21-27, 13-27-20) w
No structural problem in the Phase 3 scalar branch. The investigation directions (Vector256 mixed-content lane, BCL `Utf8.FromUtf16` comparison) remain valid academic improvements but show no meaningful production-time win — closing as Won't Fix.
## ACCORE-BIN-T-S2X9: Markerless schema lane — drop per-property type markers for fixed-shape primitives (SGen)
**Priority:** P3 · **Type:** Wire-format extension · **Related:** `ACCORE-BIN-T-S5L8`, `ACCORE-BIN-T-W7N5`
**Priority:** P2 · **Type:** Wire-format extension · **Related:** `ACCORE-BIN-T-S5L8`, `ACCORE-BIN-T-W7N5`
AcBinary is **marker-driven**: every value on the wire carries a 1-byte type code, so the reader can dispatch generically (handles polymorphism, null, intern markers, type-name lookup, etc.). MemPack is **schema-driven**: the SGen reader knows at compile time that "field 3 is `int`, field 4 is `string`" and reads values directly with no type code, no run-time dispatch.
For fixed-shape primitive properties (`int`, `bool`, `double`, `Guid`, `DateTime`, …) on `[AcBinarySerializable]` types, the per-property type marker is pure overhead — the SGen-generated reader already has compile-time knowledge of the property type, so the marker only confirms what is already known. Dropping it on this narrow class of properties is a clean wire+CPU win without losing any of the polymorphism / null / intern flexibility that the marker provides for variable-shape values.
### Why P2 — `WireMode = Fast` wire-size parity (NuGet release narrative)
The `WireMode = Fast` lane currently produces **+1.7% to +8.1% larger wire** than MemPack across all benchmark cells (AOT bench 13-40-29: Small +52 byte, Medium +474, Large +3617, Repeated +1221, Deep +581). The gap is structural: UTF-16 raw-memcpy strings are 2 bytes/char fixed, while MemPack's UTF-8 is 1 byte/char on ASCII content. Touching the string-write path to fix this would either:
- Lose the raw-memcpy guarantee (post-encode ASCII-detect + branchy dispatch — kills the FastWire CPU advantage), or
- Add sentinel-encoding micro-savings (~3-5% wire) which don't close the structural gap.
**Markerless schema lane is the only path to wire-size parity that preserves the FastWire raw-memcpy hot path.** Per-primitive-property savings (1 byte for non-tiny `int`, `Guid`, `DateTime`, `decimal`, `double`, …) compound on DTO-heavy payloads. Estimated effect on benchmark cells:
| Cell | Current FastWire | MemPack | Estimated post-S2X9 FastWire | vs MemPack |
|------|------------------|---------|------------------------------|-----------|
| Small (~70 primitive prop) | 3122 | 3070 | ~3050 | -0.7% ✅ |
| Medium (~600 primitive prop) | 10905 | 10431 | ~10300 | -1.3% ✅ |
| Large (~6000 primitive prop) | 68603 | 64986 | ~63500 | -2.3% ✅ |
| Deep (~700 primitive prop) | 15514 | 14933 | ~14800 | -0.9% ✅ |
The Repeated cell is harder to predict (string-dominated payload, fewer primitives) — likely smaller win, may not fully close the +8.1% gap. Acceptable: the Repeated cell is a string-interning stress test, not a typical DTO workload.
**NuGet release narrative**: "FastMode beats MemoryPack on **both** wire size AND throughput across all benchmark cells" — currently we have to qualify this with "throughput-only on Compact + i18n workloads"; S2X9 removes the qualifier. This is high-leverage for the public bench shootout.
### Wire savings per property type
| Type | Current encoding | Markerless lane | Wire saved |
@ -919,6 +938,7 @@ Reader-side: SGen-generated code drops the per-property `ReadByte()` + `IsTinyIn
### Acceptance
- **Primary**: `WireMode = Fast` AcBinary wire size **≤ MemPack** across Small/Medium/Large/Deep AOT benchmark cells (AOT release-publish bench is the canonical measurement)
- Wire size: ≥ -10% on DTO-heavy payloads (Guid/DateTime-rich) vs current marker-driven format
- Round-trip on the markerless lane validated on representative DTO shapes (mixed primitive + string + nested object)
- Schema-evolution fragility documented in `BINARY_FEATURES.md` (alongside the existing `PropertySkip` / default-omission caveat from `ACCORE-BIN-I-D9Y2`)

View File

@ -23,6 +23,7 @@ Start with [`BINARY_FEATURES.md`](BINARY_FEATURES.md) (overview), then [`BINARY_
- **Serialization overview** (Toon vs AcBinary vs AcJson, shared infrastructure): `../../Serializers/README.md`
- **SignalR binary transport** (uses this serializer): `../../AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md`
- **ASP.NET Core MVC formatter** (uses this serializer): `../../AyCode.Services/docs/MVC/README.md`
- **Glossary terms**: `../../../docs/GLOSSARY.md`
## Related ADRs

View File

@ -0,0 +1,20 @@
# Mvc
ASP.NET Core MVC formatters for AcBinary wire format. Standard MVC pipeline integration — works with both controller-based MVC and Minimal API.
> **Topic docs:** `AyCode.Services/docs/MVC/README.md` — media type, request/response flow, configuration, ProblemDetails error model.
> **Binary serializer:** `../../AyCode.Core/AyCode.Core/docs/BINARY/README.md`
## Key Files
- **`AcBinaryInputFormatter.cs`** — Reads request body via `PipeReader.Create(Request.Body)`, drains chunks into `AsyncPipeReaderInput` on the calling thread while a background `Task.Run` deserializes incrementally via `AcBinaryDeserializer.Deserialize(input, ModelType, opts)`. ProblemDetails error flow on failure (`ModelState.TryAddModelError` → 400 + `application/problem+json`). Cancellation honors `HttpContext.RequestAborted`.
- **`AcBinaryOutputFormatter.cs`** — Wraps `Response.Body` as `PipeWriter`, calls `AcBinarySerializer.SerializeChunked(Object, ObjectType, writer, opts)` (raw mode — pure AcBinary bytes, no per-chunk framing). `pipeWriter.CompleteAsync()` in finally.
- **`AcBinaryMvcBuilderExtensions.cs`** — `IMvcBuilder.AddAcBinaryFormatters(...)` + `IMvcCoreBuilder.AddAcBinaryFormatters(...)`. Inserts both formatters at index 0 of `MvcOptions.InputFormatters` / `OutputFormatters` so AcBinary is preferred when the client's `Accept` header allows.
## Media Type
`application/vnd.acbinary` (vendor tree). Configurable per-instance via `SupportedMediaTypes.Add(...)` if a different type is needed.
## Layering
The formatter owns the **transport-specific drain-loop** (`PipeReader.ReadAsync` → `input.Feed`). The serializer surface ends at `AsyncPipeReaderInput``AcBinaryDeserializer` itself does not know about `PipeReader`. This mirrors the SignalR pattern (`AcBinaryHubProtocol.TryParseChunkData` does the same `state.Buffer.Write(span)`-loop).

View File

@ -4,7 +4,7 @@
type = "framework"
}
Shared service implementations: SignalR communication (custom binary protocol), login services, and remote log writers.
Shared service implementations: SignalR communication (custom binary protocol), ASP.NET Core MVC formatters for the AcBinary wire format, login services, and remote log writers.
## Documentation
@ -12,6 +12,7 @@ Shared service implementations: SignalR communication (custom binary protocol),
|---|---|
| `SIGNALR/README.md` | Client-side SignalR transport (tags, wire protocol, req/resp flow) |
| `SIGNALR_BINARY_PROTOCOL/README.md` | Binary-over-SignalR wire format, chunked framing |
| `MVC/README.md` | ASP.NET Core MVC formatters for AcBinary (`application/vnd.acbinary`) |
| `LOGGING/README.md` | Remote log writers (HTTP, browser console, SignalR) |
## Folder Structure
@ -20,6 +21,7 @@ Shared service implementations: SignalR communication (custom binary protocol),
|---|---|
| [`Loggers/`](Loggers/README.md) | Remote log writers: HTTP, browser console (JS interop), SignalR |
| [`Logins/`](Logins/README.md) | Base and client-side login service implementations |
| [`Mvc/`](Mvc/README.md) | ASP.NET Core MVC `InputFormatter` / `OutputFormatter` for AcBinary wire format |
| [`SignalRs/`](SignalRs/README.md) | Custom binary SignalR protocol, client base, message tagging, serialization |
## Dependencies
@ -30,6 +32,7 @@ Shared service implementations: SignalR communication (custom binary protocol),
| `AyCode.Entities` | Entity base classes |
| `AyCode.Interfaces` | Service contracts |
| `AyCode.Models` | DTOs |
| `Microsoft.AspNetCore.App` (FrameworkReference) | ASP.NET Core MVC formatter base classes (.NET 9+) |
| `Microsoft.AspNetCore.SignalR.Client` | SignalR client |
| `Microsoft.AspNetCore.SignalR.Common` | `IHubProtocol` for custom binary protocol |
| `Microsoft.AspNetCore.Authentication.JwtBearer` | JWT authentication |

View File

@ -0,0 +1,75 @@
# MVC — AcBinary formatters
ASP.NET Core MVC `InputFormatter` / `OutputFormatter` pair for the AcBinary wire format. Works in controller-based MVC and Minimal API on .NET 9+. The wire payload is the raw `byte[]` produced by `AcBinarySerializer.Serialize(value, opts)` — bit-compatible with the single-shot byte[] API; no MVC-specific envelope.
> **Code:** `AyCode.Services/Mvc/` (`AcBinaryInputFormatter`, `AcBinaryOutputFormatter`, `AcBinaryMvcBuilderExtensions`)
> **Binary serializer:** `../../../AyCode.Core/AyCode.Core/docs/BINARY/README.md`
## Registration
```csharp
// Program.cs
builder.Services.AddControllers()
.AddAcBinaryFormatters(opts => {
opts.UseGeneratedCode = true;
});
```
`AddAcBinaryFormatters` inserts both formatters at **index 0** of `MvcOptions.InputFormatters` / `OutputFormatters` — AcBinary wins content-negotiation when the client's `Accept` header allows.
## Media Type
`application/vnd.acbinary` (vendor tree, registered with the IANA pattern but not yet IANA-listed). The same media type is sent on both request (`Content-Type`) and response.
Override via `SupportedMediaTypes.Add(...)` on a custom formatter instance if a project-specific type is needed.
## Request flow (InputFormatter)
```
HttpContext.Request.Body (Stream)
→ PipeReader.Create(Body) (PipeReader)
→ drain-loop on calling thread:
while (true) {
result = await reader.ReadAsync(ct);
foreach (segment in result.Buffer) input.Feed(segment.Span);
reader.AdvanceTo(result.Buffer.End);
if (result.IsCompleted) break;
}
input.Complete();
↑ background Task.Run feeds AcBinaryDeserializer.Deserialize(input, ModelType, opts)
→ ModelType instance → InputFormatterResult.Success
```
The drain-loop is **inline** in the formatter — the serializer surface ends at `AsyncPipeReaderInput`. Any I/O-specific draining (PipeReader, NamedPipe, FileStream, custom transport) is the consumer's responsibility.
## Response flow (OutputFormatter)
```
HttpContext.Response.Body (Stream)
→ PipeWriter.Create(Body) (PipeWriter)
→ AcBinarySerializer.SerializeChunked(value, ObjectType, writer, opts)
(raw mode — pure AcBinary bytes, no [201][UINT16] framing)
→ await pipeWriter.CompleteAsync()
```
`SerializeChunked` (not `SerializeChunkedFramed`) — the wire is a single self-contained AcBinary blob, identical to `Serialize(value, opts) → byte[]`. No multiplexed framing on the HTTP body.
## Error model
Deserialization failure → `ModelState.TryAddModelError(ModelName, ex.Message)``InputFormatterResult.Failure()`. ASP.NET pipeline emits `400 Bad Request` with `application/problem+json` (RFC 7807) — **not** an AcBinary-encoded error. The client reads the error body as JSON.
`OperationCanceledException` (when `RequestAborted` is signalled) is rethrown so the pipeline aborts the response cleanly.
## Cancellation
`HttpContext.RequestAborted` flows into both formatters. The InputFormatter passes it to `PipeReader.ReadAsync` and `Task.Run`; the OutputFormatter calls `cancellationToken.ThrowIfCancellationRequested()` after `CompleteAsync`. Mid-request abort releases all pooled resources via the `using` and `finally` blocks.
## What the formatter does NOT include
- **No Stream-async API in the binary core**`AcBinarySerializer` has no `SerializeAsync(Stream, T)` method. The formatter is the wrapper. (See `BINARY_TODO.md#accore-bin-t-t8k3` — parked.)
- **No options thread-safety guard**`AcBinarySerializerOptions` is currently mutable; if registered as a DI singleton with `Configure<>` it is fine because `Configure` is read-only at runtime, but raw-shared mutable instances across concurrent requests are unsafe. (See `BINARY_ISSUES.md#accore-bin-i-l8n5` and `BINARY_TODO.md#accore-bin-t-b7h4`.)
- **No OpenAPI metadata helpers**`Microsoft.AspNetCore.OpenApi` / `Swashbuckle.AspNetCore` pick up `SupportedMediaTypes` automatically; no extra integration needed.
## Future work
The formatter currently lives in `AyCode.Services` (alongside the SignalR transport). The intent is to extract it into its own NuGet package — `AyCode.AspNetCore.Mvc.Formatters.AcBinary` — when the binary serializer is moved to a dedicated solution. No code change required at extraction time; only project-file split.

View File

@ -7,6 +7,7 @@ Topic docs for the `AyCode.Services` project (Layer 0, service abstractions).
- [`LOGGING/`](LOGGING/README.md) — Remote logger (variant — sends log entries over the wire)
- [`SIGNALR/`](SIGNALR/README.md) — SignalR transport (tag-based protocol, generic hub methods)
- [`SIGNALR_BINARY_PROTOCOL/`](SIGNALR_BINARY_PROTOCOL/README.md) — Binary-over-SignalR wire format, chunked framing
- [`MVC/`](MVC/README.md) — ASP.NET Core MVC formatters for the AcBinary wire format (InputFormatter / OutputFormatter, `application/vnd.acbinary`)
## Navigation

View File

@ -94,7 +94,7 @@ AyCode.Services ← AyCode.Services.Server
- **AyCode.Database** — EF Core with generic DAL pattern. Session for reads, Transaction for writes. DAL pooling via `PooledDal`.
### Service Layer
- **AyCode.Services** — Client-side: SignalR client, login service, loggers.
- **AyCode.Services** — Client-side: SignalR client, login service, loggers, ASP.NET Core MVC formatters for AcBinary (`Mvc/`).
- **AyCode.Services.Server** — Server-side: SignalR hub with custom binary protocol, email (SendGrid), JWT auth.
- **AyCode.Models.Server/DynamicMethods** — Reflection-based tag→method dispatch used by the SignalR hub.