Skip to content

Hand EndpointFlow stream writes to the stack in batches - #279

Merged
rnro merged 10 commits into
apple:mainfrom
rnro:batch-endpoint-flow-writes
Oct 31, 2026
Merged

rnro merged 10 commits into
apple:mainfrom
rnro:batch-endpoint-flow-writes

Conversation

@rnro

@rnro rnro commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

EndpointFlow passed each write to the stack as it arrived. A caller submitting many small writes in a row paid the stack's per-call work once per write: a serviceStreamDataToSend, a packet-building pass, and a completion hop. Writing larger pieces avoids that cost and gives higher throughput, but callers don't always control the piece size.

Modifications:

  • Deferred the hand-off to one task queued on the context, scheduleWriteFlush. Writes queued in the same scheduler turn reach the stack together.
  • Replaced the per-request drain with writeQueuedRequests. A lone request, or one that fills the available room by itself, goes through writeSingleFrame. Any other run goes through writeFrameBatch as one FrameArray, sized once up front.
  • Made writeRequests a NetworkUniqueMarkedDeque, added in the first commit. A request that arrives with nothing uncommitted ahead of it is marked, so a later request with a higher priority cannot overtake it.
  • Unmarked the queue when the stack has no room, so a backlog waiting for room is still reordered by priority.
  • Added StreamEndpointFlowProtocol.write(_:in:) for a FrameArray.

Writes are always flushed at the end of the turn now.

NOTE: At the moment a pre-existing issue means that the room never reaches zero, so batching works but doesn't bound the send queue. A separate PR (#282) makes a QUIC stream's reported send room follow its queued bytes. This PR doesn't depend on that change.

rnro added 2 commits October 26, 2026 19:58
`EndpointFlow` has to order queued writes by priority without moving a higher-priority request ahead of ones it has already committed. A plain deque cannot say which of its front elements are committed.

* Added `NetworkUniqueMarkedDeque`, a deque with a mark over its front elements that priority insertion never crosses.
* Added `appendOrderedByPriority` for `WriteRequest`.
* Added `SwiftNetworkMarkedDequeTests`.
`EndpointFlow` passed each write to the stack as it arrived, so a burst of writes paid the stack's per-call work (`serviceStreamDataToSend`, packet building, a completion hop) once per write.

* Deferred the hand-off to one context task, `scheduleWriteFlush`, so writes queued in the same turn reach the stack together.
* Replaced the per-request drain with `writeQueuedRequests`, which hands runs of requests over in one `FrameArray` through `writeFrameBatch`.
* Made `writeRequests` a `NetworkUniqueMarkedDeque`, marking a request that has nothing uncommitted ahead of it so a later higher-priority request cannot overtake it.
* Unmarked the queue when the stack has no room, so a backlog is still reordered by priority.
* Added `StreamEndpointFlowProtocol.write(_:in:)` for a `FrameArray`.
* Added ordering tests for mixed-priority writes submitted together.
@rnro rnro added the 🔨 semver/patch No public API change. label Oct 27, 2026

@agnosticdev agnosticdev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, so if I am understanding correctly, we are batch draining the writes up to the mark and then when we do so we move the mark to the end / count of elements still waiting. Is that correct?

for _ in 0..<requestCount {
guard let writeRequest = writeRequests.popFirst() else {
break
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It "may" be more performant to do a while requestCount > 0 writeRequests.removeFirst()` type of pattern, but not blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, I'm not seeing this - what aspect of this do you hope to see a speed up for?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking, I've just found removeFirst() to be more performant than popFirst()

@rnro

rnro commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Cool, so if I am understanding correctly, we are batch draining the writes up to the mark and then when we do so we move the mark to the end / count of elements still waiting. Is that correct?

Not quite, the drain isn't bounded by the mark. writeFrameBatch takes whole requests until their total length reaches the room the stack reports - so unrelated to the mark. The mark only decides where a later higher-priority request may be inserted: never ahead of the marked prefix. It's there to preserve the existing ordering guarantees now that we do batching.

…atch

`writeFrameBatch` kept each request's completion in an `Array` of optional closures. A closure stored in a generic container is converted to its most general representation, which allocates a thunk for every element.

* Held the completions in an array of `BatchedWriteCompletion`, which keeps each closure in its natural representation.
* Skipped scheduling the delivery task when no request in the batch has a completion.
* Added tests for a batch where only some writes ask for a completion and a batch where none do.
@rnro
rnro force-pushed the batch-endpoint-flow-writes branch from 61dec0a to 939d8b6 Compare October 28, 2026 22:06
rnro added 4 commits October 28, 2026 18:24
Batching made `EndpointFlow` queue write requests briefly before handing them to the stack, so `NetworkUniqueMarkedDeque` marked the requests that must not be reordered. That retained the behavior of a priority system that is currently unused, is still in development, and may change.

Changed:
* Queued write requests in `EndpointFlow` in submission order, in a `NetworkUniqueDeque`.
* Removed `NetworkUniqueMarkedDeque`, its tests, and the marking in `addWriteRequestOnContext` and `write`.

Result:
* The batching change no longer adds a type or any ordering policy; writes keep the order they were submitted in.
…twork-evolution into batch-endpoint-flow-writes
@rnro
rnro merged commit 4d064d1 into apple:main Oct 31, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants