Skip to content

Better Akka.Stream Graph#24

Merged
st0o0 merged 25 commits into
mainfrom
feature/better-graph
Apr 15, 2026
Merged

Better Akka.Stream Graph#24
st0o0 merged 25 commits into
mainfrom
feature/better-graph

Conversation

@st0o0
Copy link
Copy Markdown
Member

@st0o0 st0o0 commented Mar 30, 2026

No description provided.

@st0o0 st0o0 self-assigned this Mar 30, 2026
@st0o0 st0o0 force-pushed the feature/better-graph branch 6 times, most recently from 885549d to 210d8fe Compare April 6, 2026 08:13
@st0o0 st0o0 force-pushed the feature/better-graph branch 14 times, most recently from 2fc4734 to 64f7d1e Compare April 13, 2026 17:32
@st0o0 st0o0 force-pushed the feature/better-graph branch from ed46d3a to 5269aaa Compare April 15, 2026 04:39
@Dirnei
Copy link
Copy Markdown
Member

Dirnei commented Apr 15, 2026

LGTM! :P

@st0o0 st0o0 force-pushed the feature/better-graph branch 2 times, most recently from 9da424e to 7da5c38 Compare April 15, 2026 11:09
st0o0 added 20 commits April 15, 2026 13:47
TASK-030-001: Record BenchmarkDotNet baseline before performance fixes

Run full comparative benchmark suite (166 benchmarks) capturing TurboHttp vs
HttpClient across HTTP/1.1 and HTTP/2.0, single-request and concurrent workloads,
light and heavy payloads, and streaming throughput. Results saved to
docs/benchmarks/baseline_030.md with raw BDN output in baseline_030_raw.txt.

Key findings: TurboHttp adds ~50-80 us overhead on single requests but scales
competitively at high concurrency (HTTP/1.1 CL=256). HTTP/2.0 has a significant
bottleneck at high concurrency levels — a primary target for Feature 030 fixes.

TASK-030-002: HPACK/QPACK Dynamic Table — LinkedList → List (CRITICAL)

TASK-030-003: HTTP/2 Request Body — Eliminate Triple-Copy (CRITICAL)

TASK-030-004: QPACK Encoder — ArrayPool for UTF-8 Encoding (CRITICAL)

Replace `Encoding.UTF8.GetBytes(string)` heap allocations in the QPACK encoder
and instruction writer with zero-allocation paths: `stackalloc` for strings ≤256
chars, `ArrayPool<byte>.Shared.Rent()` for longer strings. Rented buffers are
always returned in `finally` blocks. Eliminates 20-30 allocations per HTTP/3
request header encoding call.

TASK-030-005: VersionDispatchStage — Flow Caching per Version

Cache flow blueprints in a ConcurrentDictionary<Version, Flow> at the stage level,
shared across all substream Logic instances. On first request for a given HTTP version,
the flow factory runs and the result is cached; subsequent substreams with the same
version reuse the cached blueprint, skipping engine/transport/graph construction.

The cache is naturally bounded (max 4 entries: HTTP 1.0, 1.1, 2.0, 3.0) so no eviction
is needed. ConcurrentDictionary provides thread safety across substreams on different
dispatchers.

Also fixes a pre-existing race condition where upstream completion arriving before the
inner flow pulled the first element would cause the SubSource to complete prematurely,
dropping the buffered element.

TASK-030-006: QUIC — Remove Sequential Stream-Opening Lock (CRITICAL)

Remove `SemaphoreSlim(1)` spawn lock from `QuicConnectionManager.OpenStreamAsync` so
concurrent HTTP/3 requests open their QUIC streams independently. QUIC transport's
built-in flow control now enforces stream-count limits instead of a C# semaphore.
`EnsureProvider()` made thread-safe via double-checked lock on the existing `_lock`.
Test renamed to reflect the new concurrent-open contract.

TASK-030-007: HTTP/2 Receive Window — Default to 1MB+

- Change `Http20ConnectionStage` default `initialRecvWindowSize` from 65 535 to 1 048 576 (1 MiB)
- Add `TurboClientOptions.Http2InitialWindowSize` (default 1 MiB) so callers can tune the window
- Wire `Http2InitialWindowSize` and `MaxH2ConcurrentStreams` from options through Engine into `Http20Engine`
- Update `Http2ConnectionFlowControlSpec` threshold tests to pass explicit `initialRecvWindowSize: 65535` so their documented 16 384-byte threshold assertions remain correct under the new default

TASK-030-008: HTTP/2 Stream State Pool — Match maxConcurrentStreams

- Set `StatePoolCapacity` dynamically from `maxConcurrentStreams` (default 100, clamped to MaxStatePoolCapacity of 1000)
- Update pool capacity when server sends SETTINGS frame with new MaxConcurrentStreams value
- Pool discards states beyond capacity instead of growing unboundedly
- All 3552 unit tests pass, all 689 stream tests pass, zero build errors
- Enables efficient object recycling under high concurrency without GC churn

TASK-030-009: HPACK/QPACK — Cache UTF-8 Byte Length at Insertion

Eliminates all `Encoding.UTF8.GetByteCount()` calls that occurred after
header insertion in the HPACK dynamic table and decoder hot paths.

Changes in `HpackDecoder.cs`:
- `HpackDynamicTable._entries`: extended tuple to `(Header, NameByteLength, EncodedSize)`
  so both the name byte length and total RFC-7541-§4.1 entry size are stored once at
  insertion and reused for eviction and size-accounting — no recomputation ever.
- `HpackDynamicTable.Add()`: computes `nameByteLength` and `valueByteLength` separately,
  stores both; `ComputeEntrySize()` helper removed (inlined).
- `HpackDynamicTable.GetEntryWithSizes()`: new method returning the cached sizes alongside
  the header for callers that need them.
- `HpackStaticTable.NameByteLengths[]` / `HpackStaticTable.EncodedSizes[]`: precomputed
  parallel arrays populated in the static constructor — static table lookups now O(1) with
  zero string encoding.
- `HpackDecoder.LookupWithSizes()`: new method unifying static and dynamic table access,
  returning `(Header, NameByteLength, EncodedSize)` from cached data.
- `HpackDecoder.CheckHeaderListSizeFromEncoded()`: new overload accepting a single
  precomputed `encodedSize` integer.
- §6.1 (Indexed Header Field) decode path: replaced `Lookup` + 2× `GetByteCount` with
  `LookupWithSizes` + `CheckHeaderListSizeFromEncoded`.
- `ReadLiteralHeaderWithLengths` name-reference path: replaced `Lookup` + `GetByteCount(name)`
  with cached `NameByteLength` from `LookupWithSizes`.

QPACK (`QpackDynamicTable.cs`): already stores `(AbsoluteIndex, Entry, Size)` tuples with
`CalculateEntrySize` called once at insertion — no changes required.

Build: 0 warnings, 0 errors
Tests: 3552 unit + 689 stream — all pass

TASK-030-009: HPACK/QPACK — Cache UTF-8 Byte Length at Insertion

Pre-compute and cache the UTF-8 byte lengths of header names and values at
insertion time in both the HPACK dynamic table (HpackDecoder.cs) and the QPACK
dynamic table (QpackDynamicTable.cs). `GetByteCount()` is now called once per
header at insertion; eviction, size-tracking, and header-list-size checks all
read from the cached field. The HPACK static table pre-computes sizes at
static initialisation so no `GetByteCount()` calls occur at all for static
entries at decode time.

TASK-030-010: HTTP/3 FrameDecoder — ArrayPool Instead of new byte[]

Replace all `new byte[]` and `.ToArray()` allocations in the HTTP/3 decoder
pipeline with `ArrayPool<byte>` rented buffers and `IMemoryOwner<byte>` via
`MemoryPool<byte>` to eliminate per-frame GC pressure.

Changes:
- Http3FrameDecoder: remainder and combined working buffers now use ArrayPool;
  frame payloads (DATA, HEADERS, PUSH_PROMISE) handed to callers as
  IMemoryOwner<byte> slices via MemoryPool — zero ToArray() calls
- Http3Frame: Http3DataFrame, Http3HeadersFrame, Http3PushPromiseFrame implement
  IDisposable and hold an optional IMemoryOwner<byte> returned on Dispose()
- Http3ResponseDecoder: body assembly uses 2-pass single-alloc strategy
  (sum lengths first, then one new byte[totalLength] + direct copy) — O(n) total
- QpackInstructionDecoder: combined remainder+new-data buffer rented from
  ArrayPool; returned in finally block
- Http30DecoderStage: calls _decoder.Dispose() in PostStop() to return pooled
  remainder buffer

All 3552 unit tests and 689 stream tests pass.

TASK-030-011: HTTP/1.0 Decoder — Eliminate Excessive ToArray()

Replace 9+ intermediate `.ToArray()` allocations in `Http10Decoder` with
`ReadOnlySpan<byte>`/`ReadOnlyMemory<byte>` slicing. `Combine()` now uses
`ArrayPool<byte>.Shared.Rent()` instead of `new byte[]`. `SplitHeaderLines`,
`BuildHttp09Response`, and `BuildResponse` all accept spans directly.
Only unavoidable `.ToArray()` calls remain at the final `ByteArrayContent`
construction boundary.

TASK-030-012: HuffmanCodec — Span-Based Encode/Decode

Add `HuffmanCodec.Encode(ReadOnlySpan<byte>, Span<byte>)` and
`Decode(ReadOnlySpan<byte>, Span<byte>)` span-to-span overloads; remove
array-returning variants — every HPACK/QPACK Huffman path is now
allocation-free on the hot write path.

Update all callers (`HpackDecoder`, `HpackEncoder.WriteString`,
`QpackStringCodec`) to use `MemoryPool<byte>.Shared.Rent()` for temporary
Huffman buffers instead of `ArrayPool` or `new byte[]`.

Migrate `Http10Decoder`, `Http11Decoder`, `Http3FrameDecoder`, and
`QpackInstructionDecoder` remainder/merge buffers from `ArrayPool<byte>` to
`IMemoryOwner<byte>` via `MemoryPool<byte>.Shared`, completing the
no-`ArrayPool` invariant across all protocol decoders.

Update `HuffmanSpec`, `HpackEncodingSpec`, `HpackBombSpec` for the span API.

Build: 0 errors, 0 warnings
Tests: 3552/3552 unit, 689/689 stream — all pass

TASK-030-013: Batch Weight — Increase HTTP/2 Default to 256KB

Increase Http20Engine MaxBatchWeight from 64KB to 256KB to reduce scheduler ticks
in high-throughput scenarios while maintaining latency for small messages.

- Http20Engine: Changed MaxBatchWeight constant (now DefaultMaxBatchWeight) from 65536 to 262144
- Http20Engine: Made MaxBatchWeight configurable via constructor parameter
- TurboClientOptions: Added Http2MaxBatchWeight property (default 262144) for per-client tuning
- Engine.cs: Pass clientOptions.Http2MaxBatchWeight to Http20Engine constructor
- Http2BatchEncodingSpec: Updated test to assert new 256KB default
- All unit tests (3552) and stream tests (689) pass with Release build

TASK-030-014: MemoryStream Allocations — RecyclableMemoryStreamManager

Replace all hot-path `new MemoryStream()` allocations in protocol encoders/decoders
and content-encoding stages with pooled streams via `RecyclableMemoryStreamManager`,
reducing GC pressure from scattered temporary stream allocations.

- Add `Microsoft.IO.RecyclableMemoryStream` 3.0.1 to Central Package Management
- Introduce `TurboHttp.Internal.RecyclableStreams` static singleton
- Replace `new MemoryStream()` in ContentEncodingEncoder (×3), ContentEncodingDecoder,
  ContentEncodingBidiStage, RedirectHandler, Http10Encoder, Http3RequestEncoder
- Fix `CopyToPool` in ContentEncodingEncoder to use `ReadExactly` instead of
  `GetBuffer()` (compatible with multi-block RecyclableMemoryStream)

TASK-030-015: Per-Request Collection Allocations — ArrayPool-Backed Lists

Eliminate per-call `new List<T>()` allocations in five hot-path decoders
by reusing instance-level lists cleared at the start of each decode cycle.

- Http2FrameDecoder: `_frames` field replaces per-call `new List<Http2Frame>(10)`;
  returns `Array.Empty<T>()` for zero frames, `.ToArray()` snapshot for non-zero
  (prevents reference aliasing when tests hold prior-call result references)
- Http3FrameDecoder: `_frames` field replaces per-call `new List<Http3Frame>()`
  in `DecodeAll`; same reuse pattern
- HpackDecoder: `_headers` field replaces `new List<HpackHeader>()` in `Decode()`
- QpackDecoder: `_headers` field replaces per-call list in both `Decode()` and
  `TryDecode()` methods
- CookieJar: `_applicable` field replaces per-call `new List<CookieEntry>()`;
  sort and string-building moved inside `_lock` scope for thread safety

All 3552 unit tests and 689 stream tests pass.

TASK-030-016: TcpConnectionStage — Eliminate Task.Run per Connection

Replace `Task.Run(async () => ...)` with direct `_ = PumpAsync(...)` fire-and-forget
in both TcpConnectionStage and QuicConnectionStage. This avoids scheduling a thread-pool
work item per connection at startup, reducing overhead at high connection counts.

Adds `_onInboundPumpFailed` callback (backed by `GetAsyncCallback<Exception>(FailStage)`)
to forward unexpected pump exceptions into the stage's failure channel instead of
silently swallowing them.

TASK-030-017: QPACK Encoder Instruction Blocking — Remove Serialization

Decouple QPACK encoder instruction flushing from request frame processing
in Http30Request2FrameStage so that a backpressured encoder stream no longer
serializes the entire HTTP/3 pipeline.

Previously, TryPullUpstream required both the frame outlet AND the encoder
instruction outlet to be available before pulling the next request. When the
encoder stream was slow or full, all request processing stalled.

Now encoder instructions queue locally in an unbounded Queue<ReadOnlyMemory<byte>>
and drain opportunistically when the encoder outlet pulls. The frame outlet
operates independently — upstream pulling only requires the frame outlet to be
available and frames buffer empty. The stage completes the frame outlet as soon
as upstream finishes and frames drain, while instructions continue draining
independently until their queue is empty.

TASK-030-018: QpackStringCodec — Avoid Huffman Encode just for Length

Eliminate speculative allocation when determining whether to use Huffman
encoding: implement HuffmanCodec.GetEncodedLength() as a dry-run that
computes the encoded byte count by summing symbol bit lengths from the
HPACK Huffman table, without allocating or performing the full encode.

QpackStringCodec.Encode now checks Huffman length first; only allocates
and encodes when the Huffman path is actually shorter. For most strings,
this avoids ArrayPool allocation entirely.

Huffman encoding decision logic remains RFC 9204-compliant: uses Huffman
when shorter, otherwise uses plain literal.

All tests pass (3552 in TurboHttp.Tests, 695 in TurboHttp.StreamTests).

TASK-030-019: ConnectionManagerActor — Cache DateTime.UtcNow in Eviction Loop

Verification complete: DateTime.UtcNow caching optimization is already implemented in EvictHost method.
The eviction loop captures DateTime.UtcNow once at the start and uses the cached value for all
per-connection age comparisons, eliminating syscall overhead under high connection counts.

All acceptance criteria verified:
- DateTime.UtcNow cached at line 306 of ConnectionManagerActor.cs
- Per-connection comparisons use cached 'now' value
- Eviction behavior unchanged (all 3552 unit tests + 695 stream tests pass)
- Full Release build succeeds with zero errors

TASK-030-020: GroupByRequestEndpointStage — Avoid List Alloc in RemoveDead()

Optimize RemoveDead() to avoid allocating List<int> in the common case where
there are no dead substreams. The method now performs a first pass to check for
dead entries before allocating the list. If no dead slots are found, it returns
immediately with zero allocation.

- First pass checks for dead entries (via flag) before allocation
- Early return if no dead streams found: zero allocation, zero list creation
- Allocation and removal logic unchanged for the infrequent case with dead slots
- All substream cleanup continues to work correctly
- All 3552 unit tests pass
- All 695 stream tests pass

TASK-030-021: IClientProvider — Expose Socket Buffer Size Configuration

- Add `SocketSendBufferSize` and `SocketReceiveBufferSize` properties to `TurboClientOptions` (nullable `int?`)
- Add corresponding properties to `TcpOptions` record for transport configuration
- Apply socket buffer sizes in `TcpClientProvider.CreateSocket()` when non-null
- Update `TcpOptionsFactory.Build()` to propagate buffer size settings to TCP/TLS/QUIC options
- Defaults (null) preserve OS-selected buffer sizes, ensuring no behavioral regression

Acceptance criteria: ✅ All build and test requirements met (3552 unit tests, 695 stream tests passing)

TASK-030-022: HuffmanCodec — Static Initializer Instead of volatile (LOW)

Replaced the volatile double-check lock pattern in HuffmanCodec with a static readonly field initialized by a static method. This eliminates the volatile read barrier on every Huffman decode operation, improving performance.

Changes:
- Replaced `private static volatile HuffmanNode? _root;` with `private static readonly HuffmanNode _root = BuildTree();`
- Renamed `GetRoot()` to `BuildTree()` and simplified it to return the constructed tree
- Updated `Decode()` to use `_root` directly instead of calling `GetRoot()`
- Ensured thread safety through CLR's static initializer guarantee

All tests pass: 4334 succeeded, 0 failed

TASK-030-023: NetworkBuffer Pool — Add Capacity Cap

- Add configurable max pool size to NetworkBuffer wrapper pool (default: Environment.ProcessorCount * 2)
- Discard excess wrappers when pool reaches capacity to prevent unbounded growth under bursty load
- Expose NetworkBufferPoolSize in TurboClientOptions for operator tuning

TASK-031-001: Create per-protocol option types and update TurboClientOptions

Introduce Http1Options, Http2Options, and Http3Options as dedicated configuration
classes grouped by protocol version. TurboClientOptions now exposes Http1, Http2,
and Http3 nested properties instead of flat MaxH1*/MaxH2*/Http2* fields. All
internal call sites, benchmarks, and the feature plan updated accordingly.

TASK-031-002: Create TransportRegistry with Fluent Builder

Introduce TransportRegistry to enable test injection of mock transports by HTTP
version without modifying Engine's signature. The registry uses a fluent builder
pattern for ergonomic chaining and provides TryGet() for safe lookup.

- Internal sealed class with Dictionary-backed storage
- Register(Version, Func<Flow>) returns this for fluent chaining
- TryGet(Version, out Func) with [NotNullWhen(true)] for safe lookup
- Includes 6 unit tests demonstrating fluent registration and retrieval
- All 3552 TurboHttp.Tests and 701 TurboHttp.StreamTests pass

TASK-031-003: Rename VersionDispatchStage to EndpointDispatchStage

Change the dispatch stage to accept the full RequestEndpoint instead of
just Version, making the semantic match between the GroupBy key and the
dispatch input explicit. Cache key changes from Version to RequestEndpoint.

TASK-031-004: Extract FeaturePipelineBuilder from Engine

Move the BidiFlow feature stack composition (ContentEncoding, Cache, Expect100,
Retry, Cookie, Redirect, Handlers, Tracing layers + RequestEnricher + GraphDsl
wiring) into a dedicated FeaturePipelineBuilder internal static class. Engine now
builds the protocol core and delegates feature composition to the new builder,
enforcing the invariant that FeaturePipelineBuilder has no knowledge of
TurboClientOptions.

TASK-031-005: Extract ProtocolCoreBuilder from Engine

Move endpoint grouping, version dispatch, engine instantiation, and
transport wiring into a dedicated ProtocolCoreBuilder static class.
Engine.BuildProtocolCore and Engine.BuildConnectionFlow are deleted;
Engine now delegates to ProtocolCoreBuilder.Build and remains a thin
orchestrator wiring ProtocolCoreBuilder with FeaturePipelineBuilder.

TASK-031-006: Rewrite Engine as Thin Orchestrator + Update Test Call Sites

TASK-031-007: Update ARCHITECTURE.md to reflect Engine refactoring

Document the new builder-based architecture and updated routing topology.

- Protocol Engine Core diagram: replaced `Partition(version)` with `EndpointDispatchStage`
- TurboClientOptions row: added reference to nested `Http1Options`, `Http2Options`, `Http3Options`
- Extension Points: replaced `Engine.BuildExtendedPipeline()` with `FeaturePipelineBuilder.Build()`
- Added Builders section describing `Engine`, `ProtocolCoreBuilder`, `FeaturePipelineBuilder`
- Added `TransportRegistry` to extension points for test transport injection

TASK-032-001: Introduce ConnectionState + Delete Handler Classes

Encapsulate all HTTP/3 connection state into a single `private sealed class
ConnectionState` inside `Http30ConnectionStage`, replacing 7 external handler
classes. This aligns HTTP/3's architecture with HTTP/2's `StreamState` pattern.

## Changes

### Production Code
- **Http30ConnectionStage.cs**: Added `ConnectionState` with GoAway, ControlStream,
  IdleTimeout, and Push state. Refactored `Logic` to use `_state` instead of 7
  individual handler objects.
- **Http30ControlStreamPrefaceStage.cs**: Inlined control stream preface generation
  (previously delegated to `Http3ControlStream.OpenLocalStream()`).

### Deleted Protocol Layer Files (7 handlers)
- `Http3GoAwayHandler.cs`
- `Http3ControlStream.cs`
- `Http3IdleTimeoutHandler.cs`
- `Http3MaxPushIdHandler.cs`
- `Http3PushLimiter.cs`
- `Http3CancelPushHandler.cs`
- `Http3PushPromiseValidator.cs`

### Test Fixes
- **Http30IdleTimeoutSpec.cs**: Updated `ComputeEffectiveTimeout` calls to use
  `Http30ConnectionStage.ComputeEffectiveTimeout` (new internal static delegate).
- **Http30ControlStreamPrefaceSpec.cs**: Replaced `Http3ControlStream` verification
  with direct byte construction via `BuildExpectedPreface()` helper.
- **Deleted 8 old Protocol Layer test files** (subjects deleted, will be replaced
  by Stage-Behaviour-Tests in TASK-032-002):
  `11_ControlStreamSpec.cs`, `23_GoAwaySpec.cs`, `24_SettingsExchangeSpec.cs`,
  `26_IdleTimeoutSpec.cs`, `27_MaxPushIdSpec.cs`, `28_PushPromiseValidationSpec.cs`,
  `29_CancelPushSpec.cs`, `32_PushLimitingSpec.cs`

### Blocked
- `Http3Settings.cs` deletion blocked: TASK-032-005 (predecessor) not yet complete.
  `Http3Settings` is a wire-format type used by `Http3FrameDecoder`, `Http3Frame`,
  and the preface stage.

## Verification
- `dotnet build --configuration Release ./src/TurboHttp.sln` — 0 errors, 0 warnings

TASK-032-002: Migrate HTTP/3 handler tests to Stage-Behaviour-Tests

Replace 8 deleted Protocol Layer handler unit tests with 48 stage-behaviour
tests that push frames through Http30ConnectionStage inlets and verify
observable output on outlets. New spec files in Http3/Connection/:

- Http30ConnectionControlStreamSpec (10 tests) — SETTINGS exchange, reserved H2 settings, duplicates
- Http30ConnectionGoAwaySpec (15 tests) — GOAWAY absorption, stream ID validation, outbound dropping
- Http30ConnectionIdleTimeoutSpec (12 tests) — idle timeout expiry, timer reset, ComputeEffectiveTimeout
- Http30ConnectionPushSpec (11 tests) — CANCEL_PUSH, PUSH_PROMISE rejection, MAX_PUSH_ID absorption

All specs are sealed, use BDD method names, and carry [Trait("RFC", ...)] traceability.

TASK-032-003: Delete Http3ResponseDecoder + Migrate Tests

Remove Http3ResponseDecoder from the Protocol Layer — HTTP/2 has no
equivalent class, so response assembly belongs solely in Http30StreamStage.

- Move ValidateResponsePseudoHeaders to Http3FieldValidator as public static
- Add pseudo-header validation call to Http30StreamStage.HandleHeaders
- Create Http3FieldValidatorSpec.cs (29 tests: field name casing, connection-
  specific headers, TE rules, pseudo-header validation)
- Create Http30StreamStageConnectSpec.cs (17 tests: CONNECT encoding,
  pseudo-header validation, stage response assembly)
- Preserve encoder-only tests from 15_QpackIntegrationSpec.cs in new
  QpackIntegrationSpec.cs (7 tests)
- Delete 6 old test files (15-17, 20-22) and Http3ResponseDecoder.cs
- Fix Http3StageCompletionRegressionSpec: use valid QPACK :status 200 frame
  (index 25 = 0xD9) instead of :method GET (index 17 = 0xD1)

TASK-032-004: Add explicit null filter in Http30DecoderStage

DecodeAll() returns null for unknown frame types (RFC 9114 §7.2.8).
The loop that collected frames into the visible list was missing the
null check, so unknown frames could be forwarded downstream as null.

TASK-032-005: Add Http3SettingsIdentifier constants to Http3Frame.cs

Move and rename Http3SettingId → Http3SettingsIdentifier from Http3Settings.cs
to Http3Frame.cs, analogous to HTTP/2's SettingsParameter enum in Http2Frame.cs.
All references updated across production and test code.

TASK-033-001: QpackIntegerCodec — IBufferWriter → ref Span<byte>

Change QpackIntegerCodec.Encode from writing into IBufferWriter<byte> to
writing directly into a caller-provided ref Span<byte> and returning the
number of bytes written. This eliminates the IBufferWriter abstraction from
the innermost QPACK integer codec and is the first step of Feature 033
(Span<byte>-Only API for All Encoders and Decoders).

Callers that still use IBufferWriter (QpackStringCodec, QpackEncoder,
QpackEncoderInstructionWriter, QpackDecoderInstructionWriter) each received
a private WriteInteger bridge backed by MemoryPool<byte>.Shared.Rent(16)
that will be removed when those callers are migrated in TASK-033-003 through
TASK-033-006.

- QpackIntegerCodec.Encode: IBufferWriter<byte> → ref Span<byte>, returns int
- No IBufferWriter<byte> references remain in QpackIntegerCodec.cs
- QpackIntegerCodecSpec updated to use ref Span<byte> API directly
- QpackDecoderSpec WriteInt helper adapts new API for existing ArrayBufferWriter tests
- All 3267 unit tests pass

TASK-033-002: HuffmanCodec — Verify Span-only API (no ArrayPool overloads)

HuffmanCodec already exposes only Span-based Encode/Decode APIs with no ArrayPool
references or byte[]-returning overloads. All callers confirmed using Span-based API.
38/38 Huffman unit tests pass.

TASK-033-003: QpackStringCodec — IBufferWriter + ArrayPool → ref Span<byte>

Refactor QpackStringCodec.Encode to write directly into caller-provided Span,
encoding Huffman inline without temporary buffers. Both Encode overloads now
accept ref Span<byte> and return int bytes written. Callers (QpackEncoder,
QpackEncoderInstructionWriter) use temporary bridge methods until their own
migration in TASK-033-005/006.

TASK-033-004: HpackEncoder — IBufferWriter + ArrayPool → ref Span<byte>

Convert HpackEncoder to the zero-copy ref Span<byte> buffer strategy,
eliminating all IBufferWriter, ArrayPool, stackalloc, and new byte[]
allocations. WriteInteger and WriteString write directly into caller-
provided spans. Huffman encoding uses the reserve-max-space strategy
(write into tail of output span, compare lengths, keep shorter).
Convenience overload uses MemoryPool<byte>.Shared for its internal buffer.

TASK-033-005: QpackEncoderInstructionWriter — IBufferWriter + ArrayPool → ref Span<byte>

Migrate all 6 public methods to accept `ref Span<byte>` and return `int` bytes written.
Remove IBufferWriter<byte>, ArrayPool, and stackalloc from the file. String overloads
now use MemoryPool for UTF-8 encoding. Update callers (QpackEncoder, QpackEncoderStreamStage)
with bridge patterns and update all test files to the new API.

TASK-033-006: QpackDecoderInstructionWriter, QpackEncoder, QpackTableSync — IBufferWriter + ArrayPool → ref Span<byte>

Convert 3 QPACK files from IBufferWriter<byte>/ArrayPool output to ref Span<byte>
return-int pattern. QpackEncoder convenience overload uses MemoryPool<byte>.Shared.
Update all callers across unit tests, stream tests, and security tests.

Files changed:
- QpackDecoderInstructionWriter.cs — 3 static methods → ref Span<byte>
- QpackEncoder.cs — ArrayBufferWriter → IMemoryOwner + ref Span<byte>
- QpackTableSync.cs — WriteInsertCountIncrement → ref Span<byte>
- QpackDecoder.cs — caller update (ArrayBufferWriter → IMemoryOwner)
- 7 test files updated to match new APIs

TASK-033-007: Http3FrameEncoder — Remove IBufferWriter overloads

Remove `Encode(Http3Frame, IBufferWriter<byte>)` and `EncodeAll(IEnumerable<Http3Frame>, IBufferWriter<byte>)`
overloads from Http3FrameEncoder, leaving only the Span-based API. Updated test callers to use Span overloads.

TASK-033-008: Http2RequestEncoder — ArrayBufferWriter + ArrayPool → MemoryPool + ref Span<byte>

Replace ArrayBufferWriter and ArrayPool<byte> with MemoryPool<byte>.Shared in
Http2RequestEncoder for consistent buffer strategy across all encoders.

- Header encoding: MemoryPool rental → ref Span<byte> → HPACK encode → copy to owned array
- Body chunks: MemoryPool<byte>.Shared.Rent() replaces ArrayPool<byte>.Shared.Rent()
- DataFrame: new constructor accepting IMemoryOwner<byte> + length for pooled body data
- _rentedBodyBuffers (List<byte[]>) → _rentedBodyOwners (List<IMemoryOwner<byte>>)
- ReturnRentedBuffers() now disposes IMemoryOwner instances instead of returning to ArrayPool

TASK-033-009: Http3RequestEncoder — MemoryStream → MemoryPool + ref Span<byte>

Replace RecyclableStreams/MemoryStream + ToArray() body reading with MemoryPool<byte>
rentals. QPACK header encoding now writes directly into a MemoryPool-rented span
instead of using the convenience overload. Rented owners are tracked and disposed
on the next Encode() call, matching the Http2RequestEncoder lifecycle pattern.

TASK-033-010: Http10Decoder — ArrayPool remainder → MemoryPool

Http10Decoder already uses IMemoryOwner<byte> from MemoryPool<byte>.Shared
for its remainder buffer. No ArrayPool references exist in the file.
Verified: all 234 Http10 unit tests pass. Marked acceptance criteria complete.

TASK-033-011: Http11Decoder — verify MemoryPool migration complete

Http11Decoder already uses IMemoryOwner<byte> from MemoryPool<byte>.Shared
for both remainder and body buffers. No ArrayPool usage remains.
All 112 Http11 decoder unit tests pass.

TASK-033-012: Http3FrameDecoder + QpackInstructionDecoder — verify MemoryPool migration complete

Both files already use IMemoryOwner<byte> from MemoryPool<byte>.Shared for
remainder buffers, combined working buffers, and frame payloads. No ArrayPool
or .ToArray() remainder storage remains. Dispose patterns in Reset/IDisposable
are correct. All 41 Http3 unit tests pass.

TASK-033-013: ContentEncoding — ArrayPool → Stream-based API

ContentEncodingEncoder.Compress and ContentEncodingDecoder.Decompress now
return Stream instead of ArrayPool-rented (byte[], int) tuples.
ContentEncodingBidiStage reads streams into MemoryPool<byte> buffers and
PooledArrayContent is replaced by IMemoryOwner<byte>-backed PooledMemoryContent.
All ArrayPool usage removed from the three content-encoding files.

TASK-033-014: Streaming Stage Integration — MemoryPool migration complete

Migrate all Akka.Streams encoding stages from byte[]/new byte[] allocations
to MemoryPool<byte>.Shared.Rent() with proper IMemoryOwner<byte> lifetime
management. Stages affected: Http20EncoderStage, Http30ControlStreamPrefaceStage,
Http30QpackEncoderPrefaceStage, QpackEncoderStreamStage.

TASK-033-015: Final Cleanup + Verification — eliminate last stackalloc in Http11Encoder

Replace stackalloc temp buffers in WriteInt/WriteHex with in-place
reverse pattern, writing directly into the caller-provided Span<byte>.

Verified zero IBufferWriter<byte>, ArrayPool<byte>, ArrayBufferWriter,
and stackalloc across Protocol and Streams layers. Build and all tests
(3267 unit + 765 stream) pass.

TASK-034-001: Introduce ITransportFactory Interface

Create formal ITransportFactory interface to enable transport-agnostic pipeline design, encapsulating TCP and QUIC connection creation for HTTP/1.x, 2.0, and 3.0 protocols.

- ITransportFactory: Single Create() method returning Flow<IOutputItem, IInputItem, NotUsed>
- TcpTransportFactory: Wraps IActorRef connectionManager + TurboClientOptions
- QuicTransportFactory: Self-contained, parameterless constructor

TASK-034-002: Refactor TransportRegistry to Use ITransportFactory

- TransportRegistry now stores Dictionary<Version, ITransportFactory> instead of Func<Flow>
- Register() accepts ITransportFactory; Get() replaces TryGet and throws on missing version
- TryGet removed — single Get() method is the only API
- ProtocolCoreBuilder updated to use Get() instead of TryGet pattern
- DelegateTransportFactory adapter created in StreamTests for test convenience
- All 8 test files updated to wrap lambdas in DelegateTransportFactory
- Build verified: zero errors

TASK-034-003: Make ProtocolCoreBuilder Transport-Agnostic

Remove all Transport/ imports from ProtocolCoreBuilder by delegating transport
creation entirely to TransportRegistry. The Build signature now takes only
TurboClientOptions and TransportRegistry — no more transportStageFactory or
testTransports parameters. CreateFlowForEndpoint uses a single code path for
both production and test, eliminating the test-vs-production branch.

Engine.CreateFlow production overload now builds a TransportRegistry with
TcpTransportFactory and QuicTransportFactory before calling ProtocolCoreBuilder.

TASK-034-004: Unify Engine to Single Overload with TransportRegistry

Replace Engine's two CreateFlow overloads (production + test) with a single
transport-agnostic signature that takes TransportRegistry as the first parameter.
Transport registration moves to ClientStreamOwner, removing all TCP/QUIC imports
from Engine.cs.

TASK-034-005: Register Transports in ClientStreamOwner

ClientStreamOwner.MaterializeStream() now builds a TransportRegistry with
TcpTransportFactory and QuicTransportFactory after creating ConnectionManagerActor,
then passes it to Engine.CreateFlow(). This completes the transport-agnostic wiring
so that Engine and ProtocolCoreBuilder have zero Transport/ imports.

TASK-034-007: Update ARCHITECTURE.md to Reflect Post-031 and Post-034 Design

Documentation updated to match the actual implementation after Feature 034:

- Protocol Engine Core diagram: replaced old `Partition(version)` topology with
  actual `GroupByRequestEndpoint → EndpointDispatchStage` flow
- Builders table: `ProtocolCoreBuilder` now noted as transport-layer-agnostic
  (zero Transport imports, uses TransportRegistry for transport selection)
- Transport Layer: new `ITransportFactory` plugin section documenting the formal
  contract for transport creation (TcpTransportFactory, QuicTransportFactory,
  custom transports via Registry)
- Extension Points: added rows for `ITransportFactory` custom implementation and
  transport registry override (production + test injection unified)
- Implementation Status: HTTP/3 row updated to reflect fully wired QUIC transport
  (now 75/100, up from 60/100)

No other sections modified. All acceptance criteria verified complete.

TASK-035-001: Rename project folders, solution, and csproj files from TurboHttp to TurboHTTP

Two-step git mv (via temp name) to work around Windows case-insensitive filesystem.
Renames all 5 project folders, the solution file, and all 5 csproj files.

TASK-035-002: Update solution file and csproj references for TurboHTTP rename

Update all project references from TurboHttp to TurboHTTP across the solution
file, all csproj ProjectReference paths, InternalsVisibleTo entries, NuGet
metadata (Title, PackageProjectUrl, RepositoryUrl), and CI workflow PROJECT_PATH.

TASK-035-003: Rename all C# namespaces and using statements from TurboHttp to TurboHTTP

- Replace `namespace TurboHttp.*` → `namespace TurboHTTP.*` across all 530 .cs source files
- Replace `using TurboHttp.*` → `using TurboHTTP.*` across all .cs source files
- Fix using-alias declarations (`using Alias = TurboHttp.X` → `using Alias = TurboHTTP.X`)
- Fix `typeof(TurboHttp.X)` assembly fixture attributes in integration test collections
- Update ActivitySource string literal in TurboHttpInstrumentation: `"TurboHttp"` → `"TurboHTTP"`
- `dotnet build --configuration Release ./src/TurboHTTP.sln` succeeds with zero errors
- Public type names (TurboHttpClient, TurboHttpException, TurboHttpMetrics, etc.) unchanged

TASK-035-004: Update documentation files for TurboHTTP branding

Rename all "TurboHttp" project/brand references to "TurboHTTP" across
documentation files while preserving C# type names (TurboHttpClient,
ITurboHttpClientFactory, etc.).

Updated files:
- Root: README.md, CONTRIBUTING.md, ARCHITECTURE.md, CLAUDE.md
- VitePress config (docs/.vitepress/config.ts)
- SVG logos (aria-label attributes)
- All docs/guide/*.md, docs/architecture/*.md, docs/api/index.md
- docs/benchmarks/baseline_030.md (display text only, class names preserved)
- docs/why/index.md, docs/index.md, docs/CLAUDE.md
- LikeC4 model files, custom CSS, Vue components

TASK-035-005: Update agent definitions for TurboHTTP branding

Replace all TurboHttp project/namespace/folder references with TurboHTTP
across all 5 agent definitions (.claude/agents/). Other feature files
listed in the task plan no longer exist on disk.

TASK-035-006: Update Obsidian notes and config files for TurboHTTP branding

Replace all TurboHttp project/namespace references with TurboHTTP across
109 Obsidian vault notes and the .gitignore file. Type names
(TurboHttpClient, TurboHttpMetrics, etc.) are intentionally preserved.
The likec4 config was already correct from a prior task.

TASK-035-007: Fix remaining TurboHttp→TurboHTTP string literals and verify full build

Fixed string literal references (diagnostics source names, metrics meter name,
request option keys, comments) that were missed during the namespace rename.
All 3267 unit tests and 765 stream tests pass. Build compiles with zero errors.
@st0o0 st0o0 force-pushed the feature/better-graph branch from 1f3f37a to 11ff47c Compare April 15, 2026 11:47
@st0o0 st0o0 merged commit 7f360c0 into main Apr 15, 2026
9 checks passed
@st0o0 st0o0 deleted the feature/better-graph branch April 15, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants