Skip to content

Commit 2bc6627

Browse files
Lukasaglbrntt
andauthored
Add some benchmarks for UDP and UDP metadata (#3457)
Motivation: I'm unhappy with AddressedEnvelope.Metadata, and I plan to change it. Before I change it, I want some way to get a clear picture of whether my changes make things better or worse. Modifications: Added a bunch of benchmarks Result: Some data will be available. --------- Co-authored-by: George Barnett <[email protected]>
1 parent b60d141 commit 2bc6627

3 files changed

Lines changed: 346 additions & 0 deletions

File tree

Benchmarks/Benchmarks/NIOCoreBenchmarks/Benchmarks.swift

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,39 @@ import Benchmark
1616
import NIOCore
1717
import NIOEmbedded
1818

19+
// MARK: - Handlers for AddressedEnvelope benchmarks
20+
21+
private final class ByteBufferEnvelopeForwardingHandler: ChannelInboundHandler, Sendable {
22+
typealias InboundIn = AddressedEnvelope<ByteBuffer>
23+
24+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
25+
context.fireChannelRead(data)
26+
}
27+
}
28+
29+
private final class StringEnvelopeForwardingHandler: ChannelInboundHandler, Sendable {
30+
typealias InboundIn = AddressedEnvelope<String>
31+
32+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
33+
context.fireChannelRead(data)
34+
}
35+
}
36+
37+
// MARK: - Benchmarks
38+
1939
let benchmarks = {
40+
#if LOCAL_TESTING
41+
let defaultMetrics: [BenchmarkMetric] = [
42+
.mallocCountTotal,
43+
.contextSwitches,
44+
.wallClock,
45+
.instructions,
46+
]
47+
#else
2048
let defaultMetrics: [BenchmarkMetric] = [
2149
.mallocCountTotal
2250
]
51+
#endif
2352

2453
let leakMetrics: [BenchmarkMetric] = [
2554
.mallocCountTotal,
@@ -75,4 +104,140 @@ let benchmarks = {
75104
do { _ = try! p.futureResult.wait() }
76105
}
77106
}
107+
108+
Benchmark(
109+
"AddressedEnvelope.ByteBuffer.noMetadata",
110+
configuration: .init(
111+
metrics: defaultMetrics,
112+
scalingFactor: .kilo,
113+
maxDuration: .seconds(10_000_000),
114+
maxIterations: 10
115+
)
116+
) { benchmark in
117+
// Setup: Create channel with 20 forwarding handlers
118+
let channel = EmbeddedChannel()
119+
for _ in 0..<20 {
120+
try! channel.pipeline.syncOperations.addHandler(ByteBufferEnvelopeForwardingHandler())
121+
}
122+
123+
// Create the envelope without metadata
124+
let address = try! SocketAddress(ipAddress: "::1", port: 8080)
125+
let buffer = ByteBuffer(string: "test data")
126+
let envelope = AddressedEnvelope(remoteAddress: address, data: buffer)
127+
128+
benchmark.startMeasurement()
129+
defer {
130+
benchmark.stopMeasurement()
131+
}
132+
133+
for _ in 0..<benchmark.scaledIterations.count {
134+
try! channel.writeInbound(envelope)
135+
let result: AddressedEnvelope<ByteBuffer>? = try! channel.readInbound()
136+
blackHole(result)
137+
}
138+
}
139+
140+
Benchmark(
141+
"AddressedEnvelope.ByteBuffer.withMetadata",
142+
configuration: .init(
143+
metrics: defaultMetrics,
144+
scalingFactor: .kilo,
145+
maxDuration: .seconds(10_000_000),
146+
maxIterations: 10
147+
)
148+
) { benchmark in
149+
// Setup: Create channel with 20 forwarding handlers
150+
let channel = EmbeddedChannel()
151+
for _ in 0..<20 {
152+
try! channel.pipeline.syncOperations.addHandler(ByteBufferEnvelopeForwardingHandler())
153+
}
154+
155+
// Create the envelope with full metadata
156+
let address = try! SocketAddress(ipAddress: "::1", port: 8080)
157+
let buffer = ByteBuffer(string: "test data")
158+
let metadata = AddressedEnvelope<ByteBuffer>.Metadata(
159+
ecnState: .transportNotCapable,
160+
packetInfo: NIOPacketInfo(destinationAddress: address, interfaceIndex: 1),
161+
segmentSize: 1200
162+
)
163+
let envelope = AddressedEnvelope(remoteAddress: address, data: buffer, metadata: metadata)
164+
165+
benchmark.startMeasurement()
166+
defer {
167+
benchmark.stopMeasurement()
168+
}
169+
170+
for _ in 0..<benchmark.scaledIterations.count {
171+
try! channel.writeInbound(envelope)
172+
let result: AddressedEnvelope<ByteBuffer>? = try! channel.readInbound()
173+
blackHole(result)
174+
}
175+
}
176+
177+
Benchmark(
178+
"AddressedEnvelope.String.noMetadata",
179+
configuration: .init(
180+
metrics: defaultMetrics,
181+
scalingFactor: .kilo,
182+
maxDuration: .seconds(10_000_000),
183+
maxIterations: 10
184+
)
185+
) { benchmark in
186+
// Setup: Create channel with 20 forwarding handlers
187+
let channel = EmbeddedChannel()
188+
for _ in 0..<20 {
189+
try! channel.pipeline.syncOperations.addHandler(StringEnvelopeForwardingHandler())
190+
}
191+
192+
// Create the envelope without metadata
193+
let address = try! SocketAddress(ipAddress: "::1", port: 8080)
194+
let envelope = AddressedEnvelope(remoteAddress: address, data: "test data")
195+
196+
benchmark.startMeasurement()
197+
defer {
198+
benchmark.stopMeasurement()
199+
}
200+
201+
for _ in 0..<benchmark.scaledIterations.count {
202+
try! channel.writeInbound(envelope)
203+
let result: AddressedEnvelope<String>? = try! channel.readInbound()
204+
blackHole(result)
205+
}
206+
}
207+
208+
Benchmark(
209+
"AddressedEnvelope.String.withMetadata",
210+
configuration: .init(
211+
metrics: defaultMetrics,
212+
scalingFactor: .kilo,
213+
maxDuration: .seconds(10_000_000),
214+
maxIterations: 10
215+
)
216+
) { benchmark in
217+
// Setup: Create channel with 20 forwarding handlers
218+
let channel = EmbeddedChannel()
219+
for _ in 0..<20 {
220+
try! channel.pipeline.syncOperations.addHandler(StringEnvelopeForwardingHandler())
221+
}
222+
223+
// Create the envelope with full metadata
224+
let address = try! SocketAddress(ipAddress: "::1", port: 8080)
225+
let metadata = AddressedEnvelope<String>.Metadata(
226+
ecnState: .transportNotCapable,
227+
packetInfo: NIOPacketInfo(destinationAddress: address, interfaceIndex: 1),
228+
segmentSize: 1200
229+
)
230+
let envelope = AddressedEnvelope(remoteAddress: address, data: "test data", metadata: metadata)
231+
232+
benchmark.startMeasurement()
233+
defer {
234+
benchmark.stopMeasurement()
235+
}
236+
237+
for _ in 0..<benchmark.scaledIterations.count {
238+
try! channel.writeInbound(envelope)
239+
let result: AddressedEnvelope<String>? = try! channel.readInbound()
240+
blackHole(result)
241+
}
242+
}
78243
}

Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,36 @@ let benchmarks = {
6767
)
6868
}
6969

70+
Benchmark(
71+
"UDPEcho",
72+
configuration: .init(
73+
metrics: defaultMetrics,
74+
scalingFactor: .kilo,
75+
maxDuration: .seconds(10_000_000),
76+
maxIterations: 5
77+
)
78+
) { benchmark in
79+
try runUDPEcho(
80+
numberOfWrites: benchmark.scaledIterations.upperBound,
81+
eventLoop: eventLoop
82+
)
83+
}
84+
85+
Benchmark(
86+
"UDPEchoPacketInfo",
87+
configuration: .init(
88+
metrics: defaultMetrics,
89+
scalingFactor: .kilo,
90+
maxDuration: .seconds(10_000_000),
91+
maxIterations: 5
92+
)
93+
) { benchmark in
94+
try runUDPEchoPacketInfo(
95+
numberOfWrites: benchmark.scaledIterations.upperBound,
96+
eventLoop: eventLoop
97+
)
98+
}
99+
70100
Benchmark(
71101
"MTELG.scheduleTask(in:_:)",
72102
configuration: Benchmark.Configuration(
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
import NIOPosix
17+
18+
private final class UDPEchoHandler: ChannelInboundHandler {
19+
typealias InboundIn = AddressedEnvelope<ByteBuffer>
20+
typealias OutboundOut = AddressedEnvelope<ByteBuffer>
21+
22+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
23+
context.write(data, promise: nil)
24+
}
25+
26+
func channelReadComplete(context: ChannelHandlerContext) {
27+
context.flush()
28+
}
29+
}
30+
31+
private final class UDPEchoRequestHandler: ChannelInboundHandler {
32+
typealias InboundIn = AddressedEnvelope<ByteBuffer>
33+
typealias OutboundOut = AddressedEnvelope<ByteBuffer>
34+
35+
private let buffer = ByteBuffer(repeating: 0, count: 512)
36+
private let numberOfWrites: Int
37+
private let remoteAddress: SocketAddress
38+
private var receivedCount = 0
39+
private let readsCompletePromise: EventLoopPromise<Void>
40+
41+
init(
42+
numberOfWrites: Int,
43+
remoteAddress: SocketAddress,
44+
readsCompletePromise: EventLoopPromise<Void>
45+
) {
46+
self.numberOfWrites = numberOfWrites
47+
self.remoteAddress = remoteAddress
48+
self.readsCompletePromise = readsCompletePromise
49+
}
50+
51+
func channelActive(context: ChannelHandlerContext) {
52+
self.writeAgain(context: context)
53+
}
54+
55+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
56+
self.receivedCount += 1
57+
58+
if self.receivedCount == self.numberOfWrites {
59+
self.readsCompletePromise.succeed()
60+
} else {
61+
self.writeAgain(context: context)
62+
}
63+
}
64+
65+
private func writeAgain(context: ChannelHandlerContext) {
66+
let envelope = AddressedEnvelope(
67+
remoteAddress: self.remoteAddress,
68+
data: buffer
69+
)
70+
context.write(Self.wrapOutboundOut(envelope), promise: nil)
71+
context.flush()
72+
}
73+
}
74+
75+
func runUDPEcho(numberOfWrites: Int, eventLoop: any EventLoop) throws {
76+
let address = try SocketAddress.makeAddressResolvingHost("127.0.0.1", port: 0)
77+
78+
// Create server channel
79+
let serverChannel = try DatagramBootstrap(group: eventLoop)
80+
.channelInitializer { channel in
81+
channel.eventLoop.makeCompletedFuture {
82+
try channel.pipeline.syncOperations.addHandler(UDPEchoHandler())
83+
}
84+
}
85+
.bind(to: address)
86+
.wait()
87+
88+
let readsCompletePromise = eventLoop.makePromise(of: Void.self)
89+
90+
// Create client channel
91+
let clientChannel = try DatagramBootstrap(group: eventLoop)
92+
.channelInitializer { channel in
93+
channel.eventLoop.makeCompletedFuture {
94+
let handler = UDPEchoRequestHandler(
95+
numberOfWrites: numberOfWrites,
96+
remoteAddress: serverChannel.localAddress!,
97+
readsCompletePromise: readsCompletePromise
98+
)
99+
try channel.pipeline.syncOperations.addHandler(handler)
100+
}
101+
}
102+
.bind(to: address)
103+
.wait()
104+
105+
// Wait for all echoes to complete
106+
try readsCompletePromise.futureResult.wait()
107+
108+
// Cleanup
109+
try serverChannel.close().wait()
110+
try clientChannel.close().wait()
111+
}
112+
113+
func runUDPEchoPacketInfo(numberOfWrites: Int, eventLoop: any EventLoop) throws {
114+
let address = try SocketAddress.makeAddressResolvingHost("127.0.0.1", port: 0)
115+
116+
// Create server channel with receivePacketInfo enabled
117+
let serverChannel = try DatagramBootstrap(group: eventLoop)
118+
.channelOption(.receivePacketInfo, value: true)
119+
.channelInitializer { channel in
120+
channel.eventLoop.makeCompletedFuture {
121+
try channel.pipeline.syncOperations.addHandler(UDPEchoHandler())
122+
}
123+
}
124+
.bind(to: address)
125+
.wait()
126+
127+
let readsCompletePromise = eventLoop.makePromise(of: Void.self)
128+
129+
// Create client channel with receivePacketInfo enabled
130+
let clientChannel = try DatagramBootstrap(group: eventLoop)
131+
.channelOption(.receivePacketInfo, value: true)
132+
.channelInitializer { channel in
133+
channel.eventLoop.makeCompletedFuture {
134+
let handler = UDPEchoRequestHandler(
135+
numberOfWrites: numberOfWrites,
136+
remoteAddress: serverChannel.localAddress!,
137+
readsCompletePromise: readsCompletePromise
138+
)
139+
try channel.pipeline.syncOperations.addHandler(handler)
140+
}
141+
}
142+
.bind(to: address)
143+
.wait()
144+
145+
// Wait for all echoes to complete
146+
try readsCompletePromise.futureResult.wait()
147+
148+
// Cleanup
149+
try serverChannel.close().wait()
150+
try clientChannel.close().wait()
151+
}

0 commit comments

Comments
 (0)