Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions Sources/NIOPerformanceTester/DatagramChannelBenchmark.swift
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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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?

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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that latter idea is the way to go.

}
return 0
}
}
71 changes: 71 additions & 0 deletions Sources/NIOPerformanceTester/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1053,3 +1053,74 @@ try measureAndPrint(
iterations: 1_000_000
)
)

try measureAndPrint(
desc: "datagram_channel_bootstrap_create",
benchmark: DatagramBootstrapCreateBenchmark(
iterations: 100_000
)
)

try measureAndPrint(
desc: "datagram_channel_bind",
benchmark: DatagramChannelBindBenchmark(
iterations: 1_000
)
)

try measureAndPrint(
desc: "datagram_channel_connect",
benchmark: DatagramChannelConnectBenchmark(
iterations: 1_000
)
)

try measureAndPrint(
desc: "datagram_channel_write_unconnected_addressed",
benchmark: DatagramChannelWriteBenchmark(
iterations: 1_000,
connect: false,
envelope: true,
metadata: false
)
)

try measureAndPrint(
desc: "datagram_channel_write_connected_addressed",
benchmark: DatagramChannelWriteBenchmark(
iterations: 1_000,
connect: true,
envelope: true,
metadata: false
)
)

try measureAndPrint(
desc: "datagram_channel_write_connected_unaddressed",
benchmark: DatagramChannelWriteBenchmark(
iterations: 1_000,
connect: true,
envelope: false,
metadata: false
)
)

try measureAndPrint(
desc: "datagram_channel_write_unconnected_addressed_metadata",
benchmark: DatagramChannelWriteBenchmark(
iterations: 1_000,
connect: false,
envelope: true,
metadata: true
)
)

try measureAndPrint(
desc: "datagram_channel_write_connected_addressed_metadata",
benchmark: DatagramChannelWriteBenchmark(
iterations: 1_000,
connect: true,
envelope: true,
metadata: true
)
)