-
Notifications
You must be signed in to change notification settings - Fork 698
NIOPerformanceTester: Add benchmarks for datagram channel #2198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ee9ea6e
c504ba1
587d3be
dd06c90
5c19d6c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftNIO open source project | ||
// | ||
// Copyright (c) 2022 Apple Inc. and the SwiftNIO project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
import NIOCore | ||
import NIOPosix | ||
|
||
fileprivate final class DoNothingHandler: ChannelInboundHandler { | ||
typealias InboundIn = ByteBuffer | ||
typealias OutboundOut = ByteBuffer | ||
} | ||
|
||
class DatagramClientBenchmark { | ||
fileprivate final let group: MultiThreadedEventLoopGroup | ||
fileprivate final let serverChannel: Channel | ||
fileprivate final let localhostPickPort: SocketAddress | ||
fileprivate final let clientBootstrap: DatagramBootstrap | ||
fileprivate final let clientChannel: Channel | ||
fileprivate final let payload: NIOAny | ||
|
||
final let iterations: Int | ||
|
||
fileprivate init(iterations: Int, connect: Bool, envelope: Bool, metadata: Bool) { | ||
self.iterations = iterations | ||
|
||
self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) | ||
|
||
// server setup | ||
self.localhostPickPort = try! SocketAddress.makeAddressResolvingHost("127.0.0.1", port: 0) | ||
self.serverChannel = try! DatagramBootstrap(group: self.group) | ||
.channelInitializer { $0.pipeline.addHandler(DoNothingHandler()) } | ||
.bind(to: localhostPickPort) | ||
.wait() | ||
|
||
// client bootstrap setup | ||
self.clientBootstrap = DatagramBootstrap(group: self.group) | ||
.channelInitializer { $0.pipeline.addHandler(DoNothingHandler()) } | ||
|
||
// client channel setup | ||
if connect { | ||
self.clientChannel = try! self.clientBootstrap.connect(to: self.serverChannel.localAddress!).wait() | ||
} else { | ||
self.clientChannel = try! self.clientBootstrap.bind(to: self.localhostPickPort).wait() | ||
} | ||
|
||
// payload setup | ||
let buffer = self.clientChannel.allocator.buffer(integer: 1, as: UInt8.self) | ||
switch (envelope, metadata) { | ||
case (true, true): | ||
self.payload = NIOAny(AddressedEnvelope<ByteBuffer>( | ||
remoteAddress: self.serverChannel.localAddress!, | ||
data: buffer, | ||
metadata: .init(ecnState: .transportCapableFlag1) | ||
)) | ||
case (true, false): | ||
self.payload = NIOAny(AddressedEnvelope<ByteBuffer>( | ||
remoteAddress: self.serverChannel.localAddress!, | ||
data: buffer | ||
)) | ||
case (false, false): | ||
self.payload = NIOAny(buffer) | ||
case (false, true): | ||
preconditionFailure("No API for this") | ||
} | ||
|
||
// send one payload to activate the channel | ||
try! self.clientChannel.writeAndFlush(payload).wait() | ||
} | ||
|
||
func setUp() throws { | ||
} | ||
|
||
func tearDown() { | ||
try! self.clientChannel.close().wait() | ||
try! self.serverChannel.close().wait() | ||
try! self.group.syncShutdownGracefully() | ||
} | ||
} | ||
|
||
final class DatagramBootstrapCreateBenchmark: DatagramClientBenchmark, Benchmark { | ||
init(iterations: Int) { | ||
super.init(iterations: iterations, connect: false, envelope: true, metadata: false) | ||
} | ||
|
||
func run() throws -> Int { | ||
for _ in 1...self.iterations { | ||
_ = DatagramBootstrap(group: group) | ||
.channelInitializer { channel in | ||
channel.pipeline.addHandler(DoNothingHandler()) | ||
} | ||
} | ||
return 0 | ||
} | ||
} | ||
|
||
final class DatagramChannelBindBenchmark: DatagramClientBenchmark, Benchmark { | ||
init(iterations: Int) { | ||
super.init(iterations: iterations, connect: false, envelope: true, metadata: false) | ||
} | ||
|
||
func run() throws -> Int { | ||
for _ in 1...self.iterations { | ||
try! self.clientBootstrap.bind(to: self.localhostPickPort).flatMap { $0.close() }.wait() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, I don't think this inheritance serves us for this benchmark. |
||
} | ||
return 0 | ||
} | ||
} | ||
|
||
final class DatagramChannelConnectBenchmark: DatagramClientBenchmark, Benchmark { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nor does it really serve us much here, I think. |
||
init(iterations: Int) { | ||
super.init(iterations: iterations, connect: false, envelope: true, metadata: false) | ||
} | ||
|
||
func run() throws -> Int { | ||
for _ in 1...self.iterations { | ||
try! self.clientBootstrap.connect(to: self.serverChannel.localAddress!).flatMap { $0.close() }.wait() | ||
} | ||
return 0 | ||
} | ||
} | ||
|
||
final class DatagramChannelWriteBenchmark: DatagramClientBenchmark, Benchmark { | ||
override init(iterations: Int, connect: Bool, envelope: Bool, metadata: Bool) { | ||
super.init(iterations: iterations, connect: connect, envelope: envelope, metadata: metadata) | ||
} | ||
|
||
func run() throws -> Int { | ||
for _ in 1...self.iterations { | ||
try! self.clientChannel.writeAndFlush(self.payload).wait() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this needs some signal to delay the finishing of the test until the server has read everything. Otherwise this test will be very variable based on how fast the server is reading. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, that's an interesting one. I noticed that the current allocation benchmark we have waits for the server to read everything. However, I found that when I increased the iterations in here to get the runtime we want for the perf benchmarks, I would see some non-uniformity and some lost packets, even on localhost. IIUC there's nothing to guarantee that all the packets would arrive, even on localhost. Before opening this PR, I had something in here to assert-at-least-one-echo-response-was-received but I thought better of it. This test currently measures the client sending out the datagrams, at which point we've measured all of NIO's involvement in getting the packets out the door. You're right that we're missing some test of the read path. We could consider adding a test where the client continually sends payloads and we fulfil a promise only when the server has seen a given number of responses? WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that latter idea is the way to go. |
||
} | ||
return 0 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to inherit? It doesn't seem to use this well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right—throughout this PR—that some of these benchmark classes inherit very little from the base. This one inherits almost nothing.
The reason I went this way is because I planned to port this code over to the allocation test framework where it would be nice for everything to share some common scaffolding.
It also makes it clear that everything is set up the same for every benchmark, and then we'll just benchmark a different part of the flow in the critical loop. This is motivated by the discussion in the issue #2187. It would be nice to make it very clear that in all of these tests, we establish a control, and change and measure just one thing.
WDYT?