* Land `NIOAsyncChannel` as SPI
# Motivation
We want to provide bridges from NIO `Channel`s to Swift Concurrency. In previous PRs, we already landed the building blocks namely `NIOAsyncSequenceProducer` and `NIOAsyncWriter`. These two types are highly performant bridges between synchronous and asynchronous code that respect back-pressure.
The next step is to build convenience methods that wrap a `Channel` with these two types.
# Modification
This PR adds a new type called `NIOAsyncChannel` that is capable of wrapping a `Channel`. This is done by adding two handlers to the channel pipeline that are bridging to the `NIOAsyncSequenceProducer` and `NIOAsyncWriter`.
The new `NIOAsyncChannel` type exposes three properties. The underlying `Channel`, a `NIOAsyncChannelInboundStream` and a `NIOAsyncChannelOutboundWriter`. Using these three types the user a able to read/write into the channel using `async` methods.
Importantly, we are landing all of this behind the `@_spi(AsyncChannel`. This allows us to merge PRs while we are still working on the remaining parts such as protocol negotiation.
# Result
We have the first part necessary for our async bridges. Follow up PRs will include the following things:
1. Bootstrap support
2. Protocol negotiation support
3. Example with documentation
* Add AsyncSequence bridge to NIOAsyncChannelOutboundWriter
* Code review
* Prefix temporary spi public method
* Rename writeAndFlush to write
Motivation:
NIOLock uses a storage object constructed from a ManagedBuffer. In
general this is fine, but it's a tricky API to use safely and we want to
avoid violating any of its guarantees.
Modifications:
- Store the value in the header and the lock in the elements
- Add debug assertions on alignment
Result:
We'll be a bit more confident of the use of NIOLock
Motivation:
SwiftPM has changed its default layout for packages in
apple/swift-package-manager#6144. This breaks our CI, which assumes the
prior layout. We should work around this.
Modifications:
Enhance the code to tolerate both layouts.
Result:
Integration tests run on all platforms
Motivation:
The recently added UDP GRO tests fail on some older Linux Kernel
versions. We believe that UDP GRO support on loopback was limited in
early versions so we should tolerate those failures.
However, we've verified that on 5.15 and newer that GRO is supported so
we should not tolerate failure in those cases.
Modifications:
- Add shims to CNIOLinux to get system info via uname
- Verify GRO works before running GRO tests and if it doesn't then
validate the kernel version isn't greater than 5.15.
Results:
Less flaky tests.
* Pool buffers for messages and addresses.
* Revert changes related to controlMessageStorage
* Cosmetic fix.
---------
Co-authored-by: Cory Benfield <lukasa@apple.com>
Motivation:
Support was added for UDP_SEGMENT in #2372 which allows for large UDP
datagrams to be written to a socket by letting the kernel or NIC segment
the data across multiple datagrams. This reduces traversals across the
network stack which can lead to performance improvements. UDP_GRO is the
receive-side counterpart allowing the kernel/NIC to aggregate datagrams
and reduce network stack traversals.
Modifications:
- Add a function in CNIOLinux to check whether UDP_GRO is supported
- Add the relevant socket and channel options
- Add tests
Result:
- UDP_GRO can be enabled where supported and applications may receive
large buffers.
mark syncShutdownGracefully noasync
Motivation:
The code as-is blocks the calling thread.
Modifications:
* mark `EventLoopGroup.syncShutdownGracefully()` and `NIOThreadPool.syncShutdownGracefully()` noasync on Swift > 5.7
* offer NIOThreadPool.shutdownGracefully()
* add renamed to syncShutdownGracefully()
Motivation:
EventLoopTest has an availability annotation for the various Darwin
platforms. This availability check is repeated in a few tests which
yields warnings.
Modifications:
Remove the class level availability guard.
Result:
Warning free.
Co-authored-by: Cory Benfield <lukasa@apple.com>
Motivation:
I (foolishly) didn't validate the test fix in #2382, instead I validated
that the original test passed with 61 segments (rather than 64). The
channel needs to be recreated first.
Modifications:
- Rebuild the channel before trying again if 64 segments is too many.
Result:
Test passes.
Motivation:
On older kernel versions testWriteBufferAtGSOSegmentCountLimit fails
because the write fails with EINVAL. This appears to be a kernel bug as
it passes on more recent versions.
Modifications:
- Try again with a lower segment limit if the write fails with EINVAL.
Result:
Less flaky test.
setOption forms a raw pointer to a generic argument. The compiler will
warn on this as of:
[proposal] Constrain implicit raw pointer conversion... #1963https://github.com/apple/swift-evolution/pull/1963
/Sources/NIOPosix/BaseSocket.swift:286:31: warning: forming
'UnsafeRawPointer' to a variable of type 'T'; this is likely incorrect
because 'T' may contain an object reference.
option_value: &val,
^
Ideally, this would be fixed by adding a BitwiseCopyable constraint to
the 'value' parameter of 'BaseSocker.setOption'. That would not only
eliminate the warning, but would make the API safer. But
BitwiseCopyable isn't quite ready for public use. In the meantime,
this is a reasonable workaround.
Co-authored-by: Cory Benfield <lukasa@apple.com>
Motivation:
On Linux, the UDP_SEGMENT socket option allows for large buffers to be
written to the kernel and segmented by the kernel (or in some cases the
NIC) into smaller datagrams. This can substantially decrease the number
of syscalls.
This can be set on a per message basis on a per socket basis. This
change adds per socket configuration.
Modifications:
- Add a CNIOLinux function to check whether UDP_SEGMENT is supported on
that particular Linux.
- Add a helper to `System` to check whether UDP_SEGMENT is supported on
the current platform.
- On Linux only:
- add the udp socket option level
- add the udp_segment socket option
- Add the `DatagramSegmentSize` channel option.
- Get/Set the option in `DatagramChannel`
Results:
UDP GSO is supported on Linux.
Co-authored-by: Cory Benfield <lukasa@apple.com>
Motivation:
swift- nio was failing builds that should pass
Modifications:
Adding available to the necessary sections
* Updating test OnToRunClosure
Motivation:
testCancelledScheduledTasksDoNotHoldOnToRunClosure() was not allowed enough time and timing off at moments, causing it to fail occasionally
Modifications:
Added a ConditionLock throughout the code to make sure it only unlocks when the code has waited enough time for it to not hit the precondition failure
Motivation:
Our time types are trivial, and they should be fully transparent. This
produces minor performance improvements in code handling time types, but
is mostly useful in terms of allowing the compiler to observe that these
functions have no side effects, thereby eliding some ARC traffic.
Modifications:
Make our time types inlinable.
Result:
Better performance.
Motivation:
Channels can read `ChannelOptions.maxMessagesPerRead` times from a
socket in each read cycle. They typically re-use the same buffer for
each read and rely on it CoWing if necessary. If we read more than once
in a cycle then we may CoW the buffer. Instead of reusing one buffer we
can reuse a pool of buffers limited by `maxMessagesPerRead` and cycle
through each, reducing the chance of CoWing the buffers.
Modifications:
- Extend `RecvByteBufferAllocator` to provide the size of the next
buffer with a default implementation returning `nil`.
- Add an recv buffer pool which lazily grows up to a fixed size and
attempts to reuse buffers where possible if doing so avoids CoWing.
Results:
Fewer allocations
Motivation:
To know when we next need to wake up, we keep track of what the next
deadline will be. This works great, but in order to keep track of this
UInt64 we save off an entire ScheduledTask. This object is quite wide (6
pointers wide), and two of those pointers require ARC traffic, so doing
this saving produces unnecessary overhead.
Worse, saving this task plays poorly with task cancellation. If the
saved task is cancelled, this has the effect of "retaining" that task
until the next event loop tick. This is unlikely to produce catastrophic
bugs in real programs, where the loop does tick, but it violates our
tests which rigorously assume that we will always drop a task when it is
cancelled. In specific manufactured cases it's possible to produce leaks
of non-trivial duration.
Modifications:
- Wrote a weirdly complex test.
- Moved the implementation of Task.readyIn to a method on NIODeadline
- Saved a NIODeadline instead of a ScheduledTask
Result:
Minor performance improvement in the core event loop processing, minor
correctness improvement.
Motivation:
- Uses Concurrency but missing availability requirements; does not build
on macOS.
Modifications:
- Add availability requirements/guards
Result:
Compiles on macOS
# Motivation
We are currently always allocating a new `Deque` when we get a single element write in the `NIOAsyncWriter`
# Modification
Provide a fast path method on the `NIOAsyncWriterSinkDelegate` protocol which will be called when we receive a single element write and are currently writable.
# Result
Performance win for the single write cases.
Motivation:
PooledBuffer is an inherently unsafe type, but its original incarnation
was less safe than it needed to be. In particular, we can rewrite it to
ensure that it is compatible with automatic reference counting.
Modifications:
- Rewrite PooledBuffer to use ManagedBuffer
- Clean up alignment math
- Use scoped accessors
- Add hooks for future non-scoped access
Result:
Safer, clearer code
* Pool buffers for ivecs and storage refs in the event loop.
* Introduce PoolElement for poolable objects and add some bounds checks for the pooled buffers.
* Some polishes.
* Fix build failure with Swift 5.5/5.6
* User raw pointers instead of typed.
Motivation:
The `PendingDatagramWritesManager` unconditionally creates an array and
reserves capacity... only to never use it.
Modifications:
Remove the unsued code.
Result:
Fewer allocations.
# Motivation
Currently `testTaskCancel_whenStreaming_andNotSuspended` is flaky since `didTerminate` can be called after the iterator is dropped. Fixes https://github.com/apple/swift-nio/issues/2354
# Modification
Let's modify that slightly so we hight the condition we want to hit.
# Result
No more flaky tests.
Motivation:
The NIO docs are now published on the Swift Package Index but the README
still refers to GitHub pages.
Modifications:
- Update README and other docs to point to Swift Package Index.
Result:
Documentation links work
Motivation:
Our allocation counter tests still build for 5.0 by default. This isn't
great, but we've been getting away with it because building for 5.7
requires changing the way we express our dependencies. Doing that is a
big breaking pain in the neck that requires changes in multiple repos.
As a shorter-term goal, however, to enable testing in at least one more
repo (swift-nio-ssh), this PR lifts the version to 5.1, which is the
last release compatible with the Package.swift dependency structure we
use but that supports the Platforms we need to express.
Modifications:
- Move allocation counter to 5.1
- Add platforms from swift-nio-ssh
Result:
We can write allocation counter tests for swift-nio-ssh.
Motivation:
Currently, NIOLock and NIOLockedValueBox incur unnecessary allocation and indirection costs.
Modifications:
Create namespace LockOperations for organization
Create LockStorage<Value>, a subclass of ManagedBuffer<LockPrimitive, Value>
Store the mutex as the header and the generic value as the first and only element
Update NIOLock and NIOLockedValueBox to use LockStorage
Result:
Optimal lock and value memory layout
Fewer allocations and less indirection
Code generation is excellent
Motivation:
While the "giant buffer" test doesn't run on 32-bit systems, it does
need to compile. That means we can't set a pointer to a value that won't
fit into an Int.
Modifications:
Smaller pointers!
Result:
The compile should work again.
Motivation:
According to the Linux man page the msg_len field supposed to be used to return a number of bytes sent for the particular message.
It does not make a sense to initialize it with a size of the message.
Modifications:
Change msg_leg field initialization, use 0 instead of message size.
Result:
Use sendmmsg() call properly.
Co-authored-by: Cory Benfield <lukasa@apple.com>
# Motivation
`EmbeddedChannel` is often used in testing and currently any code under testing that uses `context.localAddress` cannot be mocked, since `EmbeddedChannelCore` is always throwing.
# Modification
Use the same values for `localAddress` in `localAddress0()`. Same for `remoteAddress`
# Result
We can now properly test code that needs local/remote addresses with `EmbeddedChannel`
Motivation:
Our CI system is beginning to struggle with allocating a giant buffer in
testSliceOfMassiveBufferWithAdvancedReaderIndexIsOk. We can work around
this by faking out the allocation.
Modifications:
- Provide a fake allocator that doesn't actually allocate memory.
- Rewrite the test to use it.
Result:
Test still validates the behaviour but doesn't touch memory anymore.
Motivation:
Empty UDP datagrams could be used to have a meaning.
Empty datagrams were being silently dropped on write.
Receiving an empty diagram causes an assertion failure (possible DDoS).
Modifications:
Remove early exit when writing empty datagrams and non-empty assertion when reading them.
Result:
We can now write and read empty datagrams.
Motivation:
Less code we have - less bugs we have.
The fix remove few lines of code keeping the same functionality.
Modifications:
Just remove some useless instance variables.
Result:
Less code.
Motivation:
ELF.wait() waits for a condition variable to become true, which can
frequently lead to extremely long waits. This is a bad thing to call on
a Swift Concurrency thread, especially as we have ELF.get() which is
preferable.
Modifications:
Make ELF.wait() unavailable from async.
Result:
Users are encouraged to use the correct primitive.