diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs index 14dcb6b..71ead3a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs @@ -28,7 +28,13 @@ public static partial class AcBinaryDeserializer public bool IsAtEnd { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _position >= _bufferLength; + get + { + if (_position < _bufferLength) return false; + // Multi-segment: try advancing to next segment before declaring end. + // ArrayBinaryInput: TryAdvanceSegment => false (JIT eliminates, same as before). + return !Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1); + } } public int Position @@ -44,7 +50,10 @@ public static partial class AcBinaryDeserializer { if (_position >= _bufferLength) { - throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position); + // Multi-segment: try advancing to next segment before giving up. + // ArrayBinaryInput: TryAdvanceSegment => false (JIT eliminates this branch). + if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1)) + throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position); } return _buffer[_position++]; @@ -55,7 +64,9 @@ public static partial class AcBinaryDeserializer { if (_position >= _bufferLength) { - throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position); + // Multi-segment: try advancing to next segment before giving up. + if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1)) + throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position); } return _buffer[_position]; @@ -210,6 +221,14 @@ public static partial class AcBinaryDeserializer { //if (FastWire) { return ReadRaw(); } + // Multi-segment safety: ensure at least 1 byte before direct buffer access. + // ArrayBinaryInput: TryAdvanceSegment => false (JIT eliminates this branch). + if (_position >= _bufferLength) + { + if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, 1)) + throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position); + } + // Fast path: single byte (0-127) - ~70% of cases var b0 = _buffer[_position]; if ((b0 & 0x80) == 0) @@ -229,7 +248,7 @@ public static partial class AcBinaryDeserializer } } - // Slow path: 3+ bytes - ~5% of cases + // Slow path: 3+ bytes or cross-segment boundary — uses ReadByte() per byte return ReadVarUIntSlow(); } diff --git a/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs index 6fbc06f..72a7026 100644 --- a/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs +++ b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs @@ -10,7 +10,7 @@ namespace AyCode.Core.Serializers.Binaries; /// Processes segments one-by-one without linearizing the entire payload. /// /// For values that span segment boundaries (e.g. a 4-byte int split across 2 segments), -/// copies the overlapping bytes into a small scratch buffer and reads from there. +/// copies the overlapping bytes into a scratch buffer and reads from there. /// /// Mirrors BufferWriterBinaryOutput pattern from the serializer side. /// @@ -21,9 +21,8 @@ public struct SequenceBinaryInput : IBinaryInputBase private readonly ArraySegment[] _segments; private int _currentSegment; - // Scratch buffer for cross-boundary reads (max 16 bytes for Guid/Decimal) + // Scratch buffer for cross-boundary reads — dynamically sized for large reads (strings, byte arrays) private byte[]? _scratchBuffer; - private int _scratchLength; // After a cross-boundary read, the next TryAdvanceSegment must load // the remainder of _currentSegment (already adjusted) without incrementing. @@ -58,7 +57,6 @@ public struct SequenceBinaryInput : IBinaryInputBase _currentSegment = 0; _scratchBuffer = null; - _scratchLength = 0; _afterCrossBoundary = false; } @@ -128,23 +126,24 @@ public struct SequenceBinaryInput : IBinaryInputBase if (_currentSegment >= _segments.Length) return false; - // Ensure scratch buffer is large enough (max 16 bytes for Guid/Decimal) - _scratchBuffer ??= new byte[32]; + var nextSeg = _segments[_currentSegment]; + var fromNext = Math.Min(needed - remaining, nextSeg.Count); + var scratchNeeded = remaining + fromNext; + + // Dynamically size scratch buffer — handles large reads (strings, byte arrays) + if (_scratchBuffer == null || _scratchBuffer.Length < scratchNeeded) + _scratchBuffer = new byte[Math.Max(32, scratchNeeded)]; // Copy tail of current segment Buffer.BlockCopy(buffer, position, _scratchBuffer, 0, remaining); // Copy head of next segment - var nextSeg = _segments[_currentSegment]; - var fromNext = Math.Min(needed - remaining, nextSeg.Count); Buffer.BlockCopy(nextSeg.Array!, nextSeg.Offset, _scratchBuffer, remaining, fromNext); - _scratchLength = remaining + fromNext; - // Set up context to read from scratch buffer buffer = _scratchBuffer; position = 0; - bufferLength = _scratchLength; + bufferLength = scratchNeeded; // Adjust the current segment to skip the bytes we already copied. // The _afterCrossBoundary flag ensures the next TryAdvanceSegment diff --git a/AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md b/AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md index 062875b..80a3d50 100644 --- a/AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md +++ b/AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md @@ -73,9 +73,11 @@ await dataSource.LoadDataSourceFromResponseData(responseData, serializerType); await dataSource.LoadItem(id); // single item by ID ``` -**Binary deserialization paths:** -- `AcObservableCollection`: `BeginUpdate()` → `BinaryToMerge()` → `EndUpdate()` — single batched UI notification. -- `List`: `BinaryTo(InnerList)` — direct populate. +**Deserialization paths:** +- **Typed response** (`T != byte[]`): protocol eagerly deserializes via `SignalDataType` → `GetResponseData()` direct cast. +- **Raw byte[] response** (`IsRawBytesData`): protocol returns raw `byte[]` → consumer deserializes: + - `AcObservableCollection`: `BeginUpdate()` → `PopulateMerge(bytes)` → `EndUpdate()` — single batched UI notification. + - `List`: `BinaryTo(InnerList)` — direct populate. **Context/Filtering:** `ContextIds` (object[]) and `FilterText` (string) are sent with every GetAll request for server-side filtering. diff --git a/AyCode.Services.Server/docs/SIGNALR_SERVER.md b/AyCode.Services.Server/docs/SIGNALR_SERVER.md index 76df27b..78ac41d 100644 --- a/AyCode.Services.Server/docs/SIGNALR_SERVER.md +++ b/AyCode.Services.Server/docs/SIGNALR_SERVER.md @@ -8,7 +8,7 @@ Server-side SignalR hub infrastructure: method dispatch, session management, bro ## Server Processing ``` -6. OnReceiveMessage(tag, requestId, signalParams, SignalData data) +6. OnReceiveMessage(tag, requestId, signalParams, object data) 7. Extract parameterBytes from signalParams.Parameters 8. DynamicMethodRegistry.GetMethodByMessageTag(tag) <- ConcurrentDictionary lookup 9. signalParams.GetParameterValues(paramInfos): @@ -18,11 +18,13 @@ Server-side SignalR hub infrastructure: method dispatch, session management, bro |- Hub validates: missing required params throw ArgumentException '- NOTE: BinaryTo only -- JSON param deserialization not supported (needs JsonTo + project ref) 10. MethodInfo.InvokeMethod(instance, params) <- unwraps Task/ValueTask -11. CreateResponseMessage(tag, Success, result) <- Binary serialize payload -> byte[] -12. SendMessageToClient(caller, tag, message, requestId): - |- Extract signalParams { Status, DataSerializerType } + SignalData from message - '- caller.OnReceiveMessage(tag, requestId, signalParams, SignalData) - (metadata + payload as separate args -- no envelope serialization) +11. ResponseToCaller(tag, Success, responseData, requestId, signalParams): +12. SendMessageToClient(caller, tag, status, responseData, requestId, clientSignalParams): + |- Build response SignalParams { Status, DataSerializerType, SignalDataType, IsRawBytesData } + |- SignalDataType = responseData?.GetType().AssemblyQualifiedName (null if IsRawBytesData) + |- IsRawBytesData forwarded from client's SignalParams + '- caller.OnReceiveMessage(tag, requestId, signalParams, responseData) + Protocol zero-copy serializes responseData directly to pipe (no intermediate byte[]) 13. If SendToOtherClientType != None: '- SendMessageToOthers(sendToOtherClientTag, result) <- uses sendToOtherClientTag, not messageTag ``` @@ -34,7 +36,7 @@ See also: `AyCode.Models.Server/DynamicMethods/README.md` ### Server-Side Lookup ``` -1. OnReceiveMessage(tag=100, requestId, signalParams, SignalData data) +1. OnReceiveMessage(tag=100, requestId, signalParams, object data) 2. DynamicMethodRegistry.GetMethodByMessageTag(100) |- Check static ConcurrentDictionary cache @@ -83,7 +85,7 @@ ConcurrentDictionary Sessions | `SendMessageToUser(userId)` | User (all connections) | | `SendMessageToUsers(userIds)` | Multiple users | -All messages serialized to `SignalData` payload + `SignalParams` metadata (Parameters=null for server->client push) -> sent as separate hub arguments via `OnReceiveMessage` (no envelope wrapping). Server wraps `byte[]` in non-pooled `SignalData`; client receives as `ArrayPool`-backed `SignalData` via `AyCodeBinaryHubProtocol`. +All messages use the same `SendMessageToClient` path: build `SignalParams` (Status, DataSerializerType, SignalDataType) + pass `object responseData` as separate hub argument. Protocol zero-copy serializes `responseData` directly to the pipe. ## Hub Events @@ -96,8 +98,6 @@ Enable with `AcWebSignalRHubBase.EnableBinaryDiagnostics = true`. Logs: hex dump (500 byte sample), header parsing (version, marker), property count + names via VarUInt reading. -`SignalResponseDataMessage.DiagnosticLogger` -- per-response logging: target type info, property list, inheritance chain, hex dump. Uses `SignalData.Span` for zero-alloc diagnostics. - ## Key Source Files | Component | Path | diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index 4b5dfe0..7bf0489 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -53,7 +53,7 @@ namespace AyCode.Services.SignalRs .ConfigureLogging(logging => { // alap minimális MS log level - logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Error); + logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug); // regisztráljuk az AcLoggerProvider-t úgy, hogy visszaadja a meglévő Logger példányt logging.AddAcLogger(_ => Logger); diff --git a/AyCode.Services/SignalRs/README.md b/AyCode.Services/SignalRs/README.md index a7ded62..bdf7b5f 100644 --- a/AyCode.Services/SignalRs/README.md +++ b/AyCode.Services/SignalRs/README.md @@ -3,20 +3,19 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and serialization helpers. > **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR.md`. -> **Binary protocol:** For wire format, zero-copy pipeline, and dual BWO pattern see `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md`. +> **Binary protocol:** For wire format, zero-copy pipeline, and three-path read logic see `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md`. ## Key Files ### Protocol -- **`AcBinaryHubProtocol.cs`** — Unsealed base `IHubProtocol` replacing JSON+Base64 with `AcBinarySerializer`. Handles all 9 SignalR message types. Uses `BufferWriterBinaryOutput` standalone mode for zero-copy writes. `byte[]`/`SignalData` fast-path bypasses serializer. `CreateByteArrayResult` virtual hook for derived protocols. Inner `SpanReader` ref struct for zero-alloc parsing. -- **`AyCodeBinaryHubProtocol.cs`** — Derived protocol. Overrides `CreateByteArrayResult` to rent from `ArrayPool` when `targetType == typeof(SignalData)`. Register this instead of `AcBinaryHubProtocol`. -- **`SignalData.cs`** — `IDisposable` wrapper for `byte[]` with optional `ArrayPool` lifecycle. `Span` for zero-copy access, `Dispose()` returns rented buffer. Created by `AyCodeBinaryHubProtocol` (pooled) or directly from `byte[]` (server send). +- **`AcBinaryHubProtocol.cs`** — Unsealed base `IHubProtocol` replacing JSON+Base64 with `AcBinarySerializer`. Handles all 9 SignalR message types. Write: `BufferWriterBinaryOutput` standalone + `AcBinarySerializer.Serialize(value, output)` zero-copy to pipe. `byte[]` fast-path writes tag+VarUInt+bytes via BWO. Read: `SequenceReader` from pipe's `ReadOnlySequence`. Three-path `ReadSingleArgument`: byte[] fast-path (0x44 tag), `IsRawBytesData` (raw byte[]), typed deser via `SignalDataType`. `_currentSignalParams` captures arg[2] for type-aware arg[3] deserialization. +- **`AyCodeBinaryHubProtocol.cs`** — Derived protocol (currently empty). Exists for registration and future project-specific hooks. Register this instead of `AcBinaryHubProtocol`. ### Client -- **`AcSignalRClientBase.cs`** — Abstract SignalR client managing `HubConnection`, request/response tracking via pooled `SignalRRequestModel`. Methods: `SendMessageToServerAsync()`, CRUD helpers (Post, Get, GetAll, GetAllInto). Configurable timeouts. -- **`IAcSignalRHubClient.cs`** — Client interface + `SignalResponseDataMessage` (sealed, `ResponseData` is `SignalData?`, supports JSON/Binary with GZip, caching, diagnostics, `Dispose()` returns buffers to ArrayPool). -- **`IAcSignalRHubBase.cs`** — Base hub interface: `OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, SignalData data)`. -- **`ISignalParams.cs`** — `ISignalParams` base interface + `SignalParams` (Status, DataSerializerType, Parameters `byte[]?`). Metadata travels as separate hub argument (AcBinary serialized), payload `SignalData` uses protocol fast-path (ArrayPool-backed). Parameters and data are independent — both nullable in any direction. +- **`AcSignalRClientBase.cs`** — Abstract SignalR client managing `HubConnection`, request/response tracking via pooled `SignalRRequestModel`. `SendCoreAsync` builds `SignalParams` (with `IsRawBytesData` when `T == byte[]`). CRUD helpers (Post, Get, GetAll, GetAllInto). Configurable timeouts. +- **`IAcSignalRHubClient.cs`** — Client interface + `SignalResponseDataMessage` (sealed, `RawResponseData` is `object?` — typed object or byte[], `GetResponseData()` direct cast). Legacy message types (`IdMessage`, `SignalPostJsonDataMessage`) marked `[Obsolete]`. +- **`IAcSignalRHubBase.cs`** — Base hub interface: `OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, object data)`. +- **`ISignalParams.cs`** — `ISignalParams` base interface + `SignalParams` (Status, DataSerializerType, Parameters `byte[]?`, SignalDataType `string?`, IsRawBytesData `bool`). Metadata travels as separate hub argument (AcBinary serialized). Parameters and data are independent — both nullable in any direction. ### Message Tagging - **`SignalMessageTagAttribute.cs`** — Three attributes: `TagAttribute` (base, int messageTag), `SignalRAttribute` (server method routing + client notification), `SignalRSendToClientAttribute` (client-side receive). @@ -25,5 +24,5 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri - **`SendToClientType.cs`** — Enum: None, Others, Caller, All. ### Serialization & Pooling -- **`SignalRSerializationHelper.cs`** — Static helpers: `SerializeToBinary()`, `DeserializeFromBinary()`, compressed JSON variants, `CreateResponseData()`. +- **`SignalRSerializationHelper.cs`** — Static helpers: `SerializeToBinary()`, `DeserializeFromBinary()`, compressed JSON variants. - **`SignalRRequestModel.cs`** — Poolable (`IResettable`) request tracking model with `ObjectPool` for reuse. diff --git a/AyCode.Services/docs/SIGNALR.md b/AyCode.Services/docs/SIGNALR.md index 10f9913..d3ead6a 100644 --- a/AyCode.Services/docs/SIGNALR.md +++ b/AyCode.Services/docs/SIGNALR.md @@ -15,13 +15,13 @@ Client ◄──OnReceiveMessage(tag, requestId, signalParams, data)── Serve ``` Tag (int) determines server method. All calls go through `OnReceiveMessage`. -Metadata (`SignalParams`) and payload (`SignalData`) travel as **separate hub arguments** — `SignalData` wraps pooled `byte[]` from `ArrayPool` via `AyCodeBinaryHubProtocol` (zero-copy fast-path), metadata is AcBinary serialized normally. +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. ``` Client: Server: AcSignalRClientBase AcWebSignalRHubBase ├─ HubConnection (WebSocket) ├─ Hub - ├─ AyCodeBinaryHubProtocol ├─ DynamicMethodRegistry + ├─ AyCodeBinaryHubProtocol ├─ DynamicMethodRegistry ├─ Pending request tracking ├─ Parameter deserialization └─ Response callbacks └─ Broadcast to other clients ``` @@ -70,9 +70,9 @@ CRUD helpers (`PostAsync`, `GetByIdAsync`, `GetAllAsync`, `PostDataAsync`) are g ### AcBinaryHubProtocol / AyCodeBinaryHubProtocol -Custom `IHubProtocol` (`"acbinary"`), replaces JSON. Zero-copy via `BufferWriterBinaryOutput` standalone mode. `byte[]` and `SignalData` args bypass serializer. +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`. -`AcBinaryHubProtocol` is the base (unsealed, generic). `AyCodeBinaryHubProtocol` derives from it and uses `ArrayPool` for `SignalData` arguments — the `CreateByteArrayResult` hook rents from pool instead of `.ToArray()`. Register `AyCodeBinaryHubProtocol` in both client and server. +`AcBinaryHubProtocol` is the base (unsealed, generic). `AyCodeBinaryHubProtocol` derives from it (currently empty — exists for registration and future project-specific hooks). Register `AyCodeBinaryHubProtocol` in both client and server. > Wire format, argument framing, dual BWO pattern, length prefix patching: `SIGNALR_BINARY_PROTOCOL.md` @@ -85,26 +85,29 @@ Custom `IHubProtocol` (`"acbinary"`), replaces JSON. Zero-copy via `BufferWriter | `Status` | SignalResponseStatus | Success/Error | | `DataSerializerType` | AcSerializerType | Binary or JsonGZip — tells client how to deserialize response data | | `Parameters` | `byte[]?` | Serialized `byte[][]` as single `byte[]` (protocol fast-path). Null when no parameters. | +| `SignalDataType` | `string?` | `AssemblyQualifiedName` of response object type. Server sets before sending. Protocol uses this for eager type-aware deserialization. Null for raw byte[] responses. | +| `IsRawBytesData` | `bool` | Client sets true when `T == byte[]` (e.g. DataSource populate/merge). Protocol returns raw byte[] without deserialization. | Typed access via methods (PostDataJson pattern): - **Client**: `SetParameterValues(object[])` — packs each param via `ToBinary()` → `byte[][]` → `byte[]` - **Server**: `GetParameterValues(ParameterInfo[])` — unpacks `byte[]` → `byte[][]` → per-element `BinaryTo(targetType)` - Protocol never sees `byte[][]` — only `byte[]`. -`SignalData data` (separate hub argument, protocol fast-path, ArrayPool-backed via `AyCodeBinaryHubProtocol`). - -`SignalData` wraps pooled `byte[]` with `IDisposable` lifecycle. Consumer accesses via `Span` (zero-copy) or `ToArray()` (copy, rare). `Dispose()` returns rented buffer to `ArrayPool` with `clearArray: true`. +`object data` (4th hub argument) — protocol handles three cases on read: +1. **byte[] fast-path**: first byte is `BinaryTypeCode.ByteArray(0x44)` → strip tag + VarUInt length → return raw payload bytes. No deserializer. +2. **IsRawBytesData**: `SignalParams.IsRawBytesData == true` → return entire argSlice as raw `byte[]`. No deserialization. Consumer handles deserialization. +3. **Typed deserialization**: resolve type from `SignalParams.SignalDataType` → `AcBinaryDeserializer.Deserialize(sequence, type)` → return typed object. `Parameters` and `data` are **independent** — both can be null or filled in any direction (SignalR is bidirectional). | Combination | Parameters | data | Example | |------------|-----------|------|---------| | Request | `byte[]` (packed params) | null/empty | client calls server method | -| Response | null | SignalData (response payload) | server returns result | -| Request + data | `byte[]` | SignalData | client responds to server with data | +| Response | null | typed object or byte[] | server returns result | +| Request + data | `byte[]` | typed object | client responds to server with data | | Signal | null | null/empty | ping, status change, broadcast trigger | -`SignalResponseDataMessage` remains as **internal DTO** for callback routing — constructed in-memory from `signalParams` + `data`, never serialized as envelope on wire. `ResponseData` is `SignalData?`. `GetResponseData()` dispatches on `DataSerializerType`: Binary → `AcBinaryDeserializer.Deserialize(Span)`, JsonGZip → decompress → `JsonTo()`. `Dispose()` returns both SignalData and JSON decompression buffers to ArrayPool. +`SignalResponseDataMessage` is an **internal DTO** for client-side callback routing and stream wire format — constructed in-memory from `signalParams` + `data`, never serialized as envelope on wire. `RawResponseData` is `object?` (typed object or byte[]). `GetResponseData()` performs direct cast. ## Request/Response Flow @@ -114,23 +117,23 @@ Typed access via methods (PostDataJson pattern): 1. PostAsync(tag, param) / PostAsync(tag, params[]) / PostDataAsync(tag, data, callback) 2. signalParams.SetParameterValues(object[]): Each param ToBinary() → byte[][] → ToBinary() → byte[] (single wire blob) -3. SignalParams { Status = Success, Parameters = byte[] } -4. HubConnection.SendAsync("OnReceiveMessage", tag, requestId, signalParams, null) +3. SignalParams { Status = Success, Parameters = byte[], IsRawBytesData = (typeof(T) == typeof(byte[])) } +4. SendCoreAsync → HubConnection.SendAsync("OnReceiveMessage", tag, requestId, signalParams, null) 5. AyCodeBinaryHubProtocol frames on wire (signalParams via AcBinary, data = null for requests) ``` ### Server → Client ``` -OnReceiveMessage(tag, requestId, signalParams, data) -├─ Construct SignalResponseDataMessage in-memory (no envelope deser): -│ └─ { Status, DataSerializerType, ResponseData (SignalData) } from signalParams + data +OnReceiveMessage(tag, requestId, signalParams, object data) +├─ Construct SignalResponseDataMessage in-memory: +│ └─ { MessageTag, Status, DataSerializerType, RawResponseData = data } ├─ Matching requestId in pending dict: │ ├─ Route: null→sync wait, Action→invoke, Func→await -│ └─ GetResponseData(): dispatches on DataSerializerType -│ Binary→Deserialize(Span), JsonGZip→Decompress→JsonTo() +│ └─ GetResponseData(): direct cast (T)RawResponseData +│ Protocol already deserialized to correct type via SignalDataType └─ No match (broadcast): - └─ abstract MessageReceived(tag, signalParams, SignalData data).Forget() + └─ abstract MessageReceived(tag, signalParams, object data).Forget() ``` Request pooling: `SignalRRequestModel` via `SignalRRequestModelPool` (ObjectPool + IResettable). @@ -149,7 +152,7 @@ GetParameterValues(ParameterInfo[]): Type-guided deserialization — each parameter is individually serialized/deserialized with its concrete type, avoiding the `object[]` → dictionary problem of untyped binary deserialization. -**Perf concern:** Per-parameter `ToBinary()`/`BinaryTo(Type)` = N× context pool acquire/release + N× type-dispatch (ThreadLocal + ConcurrentDictionary cache). For many small primitives (int, bool, string) the per-call overhead may exceed a single bulk serialization. Complex objects benefit clearly. If benchmarks show regression vs old JSON path, a batch fast-path (single serialization context for all params) should be added. +**Perf concern:** Per-parameter `ToBinary()`/`BinaryTo(Type)` = N× context pool acquire/release + N× type-dispatch (ThreadLocal + ConcurrentDictionary cache). For many small primitives (int, bool, string) the per-call overhead may exceed a single bulk serialization call. Complex objects benefit clearly. If benchmarks show regression vs old JSON path, a batch fast-path (single serialization context for all params) should be added. **Limitation:** Parameter serialization/deserialization is currently AcBinary only (`ToBinary()`/`BinaryTo()`). JSON support would require dispatching on serializer type in `SignalParams` methods + AcJsonSerializer reference. @@ -179,7 +182,6 @@ Type-guided deserialization — each parameter is individually serialized/deseri | Client base | `SignalRs/AcSignalRClientBase.cs` | | Binary protocol (base) | `SignalRs/AcBinaryHubProtocol.cs` | | Binary protocol (derived) | `SignalRs/AyCodeBinaryHubProtocol.cs` | -| Signal data wrapper | `SignalRs/SignalData.cs` | | Tag attributes | `SignalRs/SignalMessageTagAttribute.cs` | | Base tags | `SignalRs/AcSignalRTags.cs` | | CRUD tags | `SignalRs/SignalRCrudTags.cs` | diff --git a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md index 9867f5b..6621193 100644 --- a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md +++ b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md @@ -1,6 +1,6 @@ # SignalR Binary Protocol -`AcBinaryHubProtocol` (unsealed base) — custom `IHubProtocol` (name: `"acbinary"`) replacing SignalR JSON+Base64 with `AcBinarySerializer`. `AyCodeBinaryHubProtocol` (derived) adds `ArrayPool`-backed `SignalData` creation via `CreateByteArrayResult` hook. +`AcBinaryHubProtocol` (unsealed base) — custom `IHubProtocol` (name: `"acbinary"`) replacing SignalR JSON+Base64 with `AcBinarySerializer`. `AyCodeBinaryHubProtocol` (derived, currently empty) exists for registration and future project-specific hooks. > Architecture (tag system, dispatch, request/response): `SIGNALR.md` > Output writers (cached chunk, buffer states, chunk sizing): `AyCode.Core/docs/BINARY_WRITERS.md` @@ -41,7 +41,7 @@ WriteMessage(HubMessage, IBufferWriter output) │ ├─ WriteStringUtf8(invocationId, target) │ ├─ WriteVarUInt(argCount) │ ├─ Per argument: -│ │ ├─ byte[] → write through BWO (size known, no patching) +│ │ ├─ byte[] → byte[] fast-path through BWO (size known, no patching) │ │ └─ object → FlushAndReset() → reserve INT32 arg prefix │ │ → AcBinarySerializer.Serialize(value, output) → patch prefix │ ├─ WriteStringArray(streamIds) @@ -77,26 +77,78 @@ Safe for `PipeWriter` — segments writable until `FlushAsync`. **`GetMessageBytes` caveat:** `ArrayBufferWriter` initial capacity must include `LengthPrefixSize` to prevent resize after prefix reservation (stale span). -## byte[] Fast-Path +## Write: byte[] Fast-Path -When argument is `byte[]`, bypasses serializer: -1. Size upfront: `1 (BinaryTypeCode) + VarUIntSize(length) + length` -2. INT32 prefix written with actual value (no patching) -3. `BinaryTypeCode.ByteArray(68)` + VarUInt length + raw bytes via BWO +When argument is `byte[]`, bypasses serializer entirely — writes through BWO with known size: -Read side mirrors: if first byte is `ByteArray(0x44)`, deserializer bypassed → direct `SpanReader` → `CreateByteArrayResult(span, targetType)`. Base returns `data.ToArray()`. `AyCodeBinaryHubProtocol` overrides: if `targetType == typeof(SignalData)`, rents from `ArrayPool` and returns `SignalData(rented, length, isRented: true)`. Detection is **wire-format only** (no targetType check for the marker) — ByteArray marker is unambiguous since no AcBinary object starts with 0x44 (version=1). +``` +WriteArgument(byte[] value): + argPayload = 1 (BinaryTypeCode) + VarUIntSize(length) + length + Write INT32 argPayload (no patching needed — size known upfront) + Write BinaryTypeCode.ByteArray (0x44) + Write VarUInt length + Write raw bytes via BWO +``` -Write side: `WriteArgument` handles both `byte[]` and `SignalData` via the same ByteArray wire format. `SignalData.Span` is written directly — same marker + VarUInt length + raw bytes. +## Write: Object Zero-Copy Path -## Read Path +When argument is any other object, serializes directly to the pipe (zero-copy): -`SpanReader` — `ref struct` for sequential `ReadOnlySpan` reading: +``` +WriteArgument(object value): + FlushAndReset() BWO — hand pipe to serializer + Reserve INT32 arg length prefix on pipe + AcBinarySerializer.Serialize(value, output, options) — writes directly to pipe + Patch arg length prefix with actual bytes written +``` -1. Read INT32 length. If `input.Length < total` → false (incomplete). -2. Multi-segment `ReadOnlySequence` → rent contiguous buffer from `ArrayPool`. -3. Parse message type → type-specific parser. -4. Fields via `SpanReader` methods (`ReadByte`, `ReadString`, `ReadVarUInt`, `ReadInt32`, `ReadInt64`, `ReadSpan`). -5. Arguments: INT32 length → slice → `AcBinaryDeserializer.Deserialize(span, targetType)`. +No intermediate `byte[]` — serializer writes to the pipe's `IBufferWriter` segments. + +## Read: Three-Path Argument Deserialization + +`ReadSingleArgument` reads `[INT32 argLength] [argBytes]` from the pipe's `ReadOnlySequence` via `SequenceReader`: + +``` +ReadSingleArgument(SequenceReader, targetType): + Read INT32 argLength + if argLength == 0 → return null + if argLength == 1 && first byte == 0 → return null (null marker) + + argSlice = UnreadSequence.Slice(0, argLength) — zero-copy reference + Advance(argLength) + + 1. byte[] fast-path: + if first byte == BinaryTypeCode.ByteArray (0x44): + skip tag + VarUInt length → return payload as byte[] + Detection is wire-format only — 0x44 is unambiguous (no AcBinary object starts with it) + + 2. IsRawBytesData path: + if _currentSignalParams.IsRawBytesData == true: + return SequenceToByteArray(argSlice) — entire arg as raw byte[], no deserialization + Consumer (DataSource.PopulateMerge) handles deserialization + + 3. Typed deserialization: + if targetType == object && SignalDataType != null: + resolve Type from SignalDataType (AssemblyQualifiedName) + DeserializeFromSequence(argSlice, resolvedType, options) + → AcBinaryDeserializer.Deserialize(ReadOnlySequence, Type) + → single-segment: ArrayBinaryInput (zero-copy via TryGetArray) + → multi-segment: SequenceBinaryInput (lazy iteration, no pre-allocation) +``` + +### SignalParams Capture + +`_currentSignalParams` field captures the parsed `SignalParams` (arg[2]) during `ReadArguments`. The 4th arg (data) uses it for type-aware deserialization. Thread-safe: SignalR processes messages sequentially per connection. + +### SequenceToByteArray + +Zero-copy when possible: if single-segment and backing array matches exactly → return the array directly. Otherwise `ReadOnlySequence.ToArray()`. + +### 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. + +Cross-boundary reads (e.g. 4-byte int split across 2 segments) use a small scratch buffer (32 bytes). Remainder tracking via `_remainderArray/Offset/Count` — no segment array mutation. ## Config @@ -105,4 +157,4 @@ Write side: `WriteArgument` handles both `byte[]` and `SignalData` via the same | `Options` | `AcBinarySerializerOptions.Default` | Serializer options (volatile, runtime-replaceable) | | `BufferWriterChunkSize` | 65536 | Chunk size for both BWOs | -**Source:** `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs` (base), `AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs` (derived, ArrayPool) +**Source:** `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs` (base), `AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs` (derived) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c57a867..958330c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -36,7 +36,7 @@ AyCode.Services ← AyCode.Services.Server - **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. -> **SignalR Dispatch:** Both directions use a single method `OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, SignalData data)` with integer tag-based routing instead of standard Hub methods. `SignalData` wraps `ArrayPool`-backed byte buffers for zero-alloc receive path. See `AyCode.Services/docs/SIGNALR.md` for full details. +> **SignalR Dispatch:** Both directions use a single method `OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, object data)` with integer tag-based routing instead of standard Hub methods. Write path: zero-copy via `AcBinarySerializer.Serialize(value, output)` directly to pipe. Read path: protocol eagerly deserializes `data` to typed object via `SignalParams.SignalDataType`, or returns raw `byte[]` for `IsRawBytesData`/byte[] fast-path. See `AyCode.Services/docs/SIGNALR.md` for full details. ### Server Extensions - **AyCode.Core.Server**, **AyCode.Interfaces.Server**, **AyCode.Entities.Server**, **AyCode.Models.Server** — Server-only additions that don't belong in shared code. diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index 4157613..74dd35f 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -26,10 +26,10 @@ See `AyCode.Services/docs/SIGNALR.md` for full architecture documentation. -- **Single dispatch method** — all communication goes through `OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, SignalData data)`. Do not add new hub methods. +- **Single dispatch method** — all communication goes through `OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, object data)`. Do not add new hub methods. - **Tag-based routing** — associate methods with integer tags via `[SignalR(tag)]` (server) or `[SignalRSendToClient(tag)]` (client). Tags must be unique across the entire system. - **CRUD bundles** — entities use `SignalRCrudTags(getAllTag, getItemTag, addTag, updateTag, removeTag)` with 5 independent tag integers. Tags must be unique across the system. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`. -- **Binary protocol** — `AyCodeBinaryHubProtocol` (derived from `AcBinaryHubProtocol`) is the transport protocol. Uses `ArrayPool`-backed `SignalData` for response payload. Responses use pure Binary serialization. +- **Binary protocol** — `AyCodeBinaryHubProtocol` (derived from `AcBinaryHubProtocol`) is the transport protocol. Zero-copy write: `AcBinarySerializer.Serialize(value, output)` directly to pipe. Zero-copy read: `SequenceReader` + type-aware deserialization via `SignalParams.SignalDataType`. Three read paths: byte[] fast-path (0x44 tag), IsRawBytesData (raw byte[]), typed deserialization. ### ⚠️ Temporary: JSON-in-Binary Request Parameters diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 6d98133..506c6b8 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -32,6 +32,8 @@ For full specification see `AyCode.Core/docs/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. Cross-boundary reads use scratch buffer. Used by `AcBinaryDeserializer.Deserialize(ReadOnlySequence)` for multi-segment data. | +| **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`. | @@ -70,21 +72,20 @@ For full architecture see `AyCode.Services/docs/SIGNALR.md`. | Term | Definition | |---|---| -| **OnReceiveMessage** | The single SignalR method for all communication. Signature: `(int messageTag, int? requestId, SignalParams signalParams, SignalData data)`. Metadata and payload are separate hub arguments — `SignalData` uses protocol zero-copy fast-path with `ArrayPool` backing. `SignalParams.Parameters` carries packed method params as `byte[]`. `data` carries response payload. Both are independent and nullable in any direction. | -| **SignalParams** | Metadata sent alongside message payload as separate hub argument. Contains `Status`, `DataSerializerType`, and `Parameters` (`byte[]?` — packed `byte[][]` as single blob, protocol fast-path). Typed access via `SetParameterValues(object[])` / `GetParameterValues(ParameterInfo[])` — PostDataJson pattern. `[AcBinarySerializable]`. Never null — only `Parameters` inside is nullable. | +| **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. | | **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.md`. | -| **AcBinaryHubProtocol** | Unsealed base `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. Uses `BufferWriterBinaryOutput` for zero-copy writes. `CreateByteArrayResult` virtual hook for derived protocols. | -| **AyCodeBinaryHubProtocol** | Derived protocol. Overrides `CreateByteArrayResult`: when `targetType == typeof(SignalData)`, rents from `ArrayPool` instead of `.ToArray()`. Register this in both client and server. | -| **SignalData** | `IDisposable` wrapper for `byte[]` response data with optional `ArrayPool` lifecycle. `Span` for zero-copy read access, `ToArray()` for copy (rare). `Dispose()` returns rented buffer with `clearArray: true`. Created by `AyCodeBinaryHubProtocol` (pooled) or directly from `byte[]` (server send, non-pooled). | -| **SignalResponseDataMessage** | Internal DTO for callback routing (not serialized on wire). Constructed in-memory from `signalParams` + `SignalData`. `ResponseData` is `SignalData?`. `GetResponseData()` deserializes from `Span`. `Dispose()` returns both SignalData and JSON decompression buffers to ArrayPool. | -| **SignalPostJsonDataMessage** | OBSOLETE — removed. Legacy: serialized params to JSON inside Binary envelope. | +| **AcBinaryHubProtocol** | Unsealed base `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. Write: `BufferWriterBinaryOutput` standalone + `AcBinarySerializer.Serialize(value, output)` zero-copy to pipe. Read: `SequenceReader` from pipe's `ReadOnlySequence`, three-path argument deser (byte[] fast-path, IsRawBytesData, typed via SignalDataType). `_currentSignalParams` captures arg[2] for type-aware arg[3] deserialization. | +| **AyCodeBinaryHubProtocol** | Derived protocol (currently empty). Exists for registration and future project-specific hooks. Register this in both client and server. | +| **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. | | **AcSignalRDataSource** | Generic real-time `IList` with change tracking, CRUD via SignalRCrudTags, binary merge, rollback, sync state. | | **TrackingItem** | Wraps a modified DataSource item with `TrackingState` (Add/Update/Remove) + `OriginalValue` for rollback. | | **SendToClientType** | Enum controlling broadcast scope: None, Others, Caller, All. | -| **AcWebSignalRHubBase** | Abstract server hub. Receives `OnReceiveMessage` (with `SignalData`), dispatches via DynamicMethodRegistry, responds/broadcasts via `SendMessageToClient` (metadata + `SignalData` payload as separate args, no envelope). | -| **AcSignalRClientBase** | Abstract client. Manages `HubConnection`, request/response correlation via `requestId`, pooled `SignalRRequestModel`. | +| **AcWebSignalRHubBase** | Abstract server hub. Receives `OnReceiveMessage` (with `object data`), dispatches via DynamicMethodRegistry, responds via `SendMessageToClient` (builds response `SignalParams` with `SignalDataType` + passes `object responseData` — protocol zero-copy serializes to pipe). | +| **AcSignalRClientBase** | Abstract client. Manages `HubConnection`, request/response correlation via `requestId`, pooled `SignalRRequestModel`. `SendCoreAsync` builds `SignalParams` (with `IsRawBytesData` when `T == byte[]`). | | **AcSessionService** | `ConcurrentDictionary`-based session tracker for connected SignalR clients. | | **AcSignalRSendToClientService** | Server-push service: `SendMessageToAllClients`, `SendMessageToConnection`, `SendMessageToUser`. | | **Working Reference List** | DataSource feature: `SetWorkingReferenceList()` allows external list to become inner storage (zero-copy). |