Skip to content

Tier-3 I/O and allocation optimizations - #120

Open
mshehan wants to merge 4 commits into
ms/feature-swift6Updatefrom
ms/tier-3-io-optimizations
Open

mshehan wants to merge 4 commits into
ms/feature-swift6Updatefrom
ms/tier-3-io-optimizations

Conversation

@mshehan

@mshehan mshehan commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Performance-focused pass over the I/O hot path and small allocation sites, plus a behavior-preserving idiom cleanup. No public API changes beyond IOEvent becoming an OptionSet and DefaultHTTPServer.acceptSocket being a true Optional.

Changes

  • Drain Transport's outgoing buffer by offset instead of removeFirst, avoiding a memmove of the remaining body on every partial write; compact the consumed prefix past 64 KB; register the writer callback once per backlog instead of on every partial write; share one tearDown across the three close paths
  • Make IOEvent an OptionSet (single byte) instead of Set<enum>, removing per-registration and per-loop-iteration allocation and hashing
  • Build the environ dictionary and response headers in one pass instead of merging a second dictionary; drop MultiDictionary's duplicated init loop; HTTPConnection loses its implicitly-unwrapped state (eventLoop is a let, headerParser is lazy, request is local) and logs + closes on a malformed header block instead of trapping
  • Idiom cleanup pass: embassy.version now comes from an Embassy.version constant (the old Bundle(identifier:) lookup never resolved under SwiftPM); DefaultHTTPServer.acceptSocket is a real Optional so the server can be restarted and listenAddress fails with a message instead of a nil unwrap; KqueueSelector.register/unregister share one kevent-building helper; switch expressions and other minor idiom updates

Test plan

  • swift build / swift test pass
  • KqueueSelectorTests updated for the new IOEvent OptionSet shape

mshehan and others added 4 commits September 23, 2026 11:16
Transport.handleWrite removed sent bytes from the front of outgoingBuffer
after every partial write, which memmoves the whole remaining body each
time the socket buffer fills. Track an offset instead, release the
storage only when fully drained (keeping capacity), and compact the
consumed prefix on append once it passes 64 KB.

The writer callback is also registered once per backlog instead of on
every partial write; each set/remove was two kevent syscalls. The three
close paths share one tearDown.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SelectorKey.events and every select() result carried a Set<IOEvent> for
two possible flags, allocating and hashing on each registration and each
loop iteration. IOEvent is now an OptionSet over a UInt8 with the same
.read / .write spellings; array literals, union, subtracting, contains
and isEmpty all keep working. An `elements` accessor covers the two
places that iterate.

Source-breaking only for Selector conformers, of which there are none
outside this package.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SWSGIUtils built a second [String: Any] for the HTTP_* header keys and
merged it into environ through a private += operator; write the keys
straight into environ. HTTPConnection.startResponse built a whole
MultiDictionary to test for two header names; a case-insensitive linear
scan over a handful of headers does the same without allocating.
MultiDictionary's two inits shared a duplicated loop, now one init using
the default-value subscript.

HTTPConnection also loses its implicitly unwrapped state: eventLoop is a
let, headerParser is a lazy var, and request is a local. A header block
whose first element is not the request line now logs and closes instead
of trapping on a nil unwrap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Behavior-preserving modernizations across Sources:

- embassy.version comes from a new Embassy.version constant. The old
  Bundle(identifier:) lookup never resolved under SwiftPM, so every
  request reported "unknown"; bump the constant when tagging.
- DefaultHTTPServer.acceptSocket is a real Optional. stop() resets it so
  a server can be restarted, and listenAddress fails with a message
  instead of a nil unwrap when read before start().
- KqueueSelector.register/unregister share one kevent-building helper.
- HTTPRequest.Method and LogLevel.name use switch expressions; single-
  expression bodies drop their explicit return; `.count > 0` becomes
  `!isEmpty`; String.Encoding.utf8 spelled `.utf8`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

mshehan commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@mshehan mshehan changed the title Drain the outgoing buffer by offset Tier-3 I/O and allocation optimizations Sep 23, 2026
@mshehan
mshehan marked this pull request as ready for review September 23, 2026 18:41

@joshua-envoy joshua-envoy left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM. The Transport offset/compaction and the writerRegistered tracking look correct, and the environ rewrite is equivalent to the old one. Non-blocking:

  • public enum Embassy in Version.swift has the same name as the module, so module-qualified references like Embassy.DefaultHTTPServer or Embassy.Logger stop compiling ('not a member type of enum Embassy.Embassy'). Nothing downstream qualifies names today, but I'd suggest renaming it (e.g. EmbassyVersion) before 5.0.0 final.
  • start() now retries properly after a failed bind, which is an improvement. But envoy-ipad's UITestBase.setupWebApp calls start() in a repeat … while loop with no backoff, so a port that stays taken would spin hot and flood /tmp/embassy.log. That's a follow-up on the iPad side.
  • The description says a malformed header block is now logged and closed. However, HTTPHeaderParser still traps on parts[1] for a request line with no spaces, such as a leading bare CRLF. This predates the PR.

@fangpenlin fangpenlin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lgtm

mshehan commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Oct 2, 9:07 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Oct 2, 9:09 PM UTC: Graphite couldn't merge this pull request because a downstack PR Drop Linux support #117 failed to merge.

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.

4 participants