Skip to content

Commit cc18bbf

Browse files
test: Add ported benchmarks from NIOPosix - Pull request #12
2 parents 7381114 + 0c39056 commit cc18bbf

File tree

7 files changed

+383
-0
lines changed

7 files changed

+383
-0
lines changed

Benchmarks/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
9+
.benchmarkBaselines/
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Copyright (c) 2025 PassiveLogic, Inc.
4+
// Licensed under Apache License v2.0
5+
//
6+
// See LICENSE.txt for license information
7+
//
8+
// SPDX-License-Identifier: Apache-2.0
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
// NOTE: By and large the benchmarks here were ported from swift-nio
13+
// to allow side-by-side performance comparison
14+
//
15+
// See https://github.com/apple/swift-nio/blob/main/Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift
16+
17+
import Benchmark
18+
import NIOAsyncRuntime
19+
import NIOCore
20+
21+
private let eventLoop = MultiThreadedEventLoopGroup.singleton.next()
22+
23+
let benchmarks = {
24+
let defaultMetrics: [BenchmarkMetric] = [
25+
.mallocCountTotal,
26+
.contextSwitches,
27+
.wallClock,
28+
]
29+
30+
Benchmark(
31+
"MTELG.immediateTasksThroughput",
32+
configuration: Benchmark.Configuration(
33+
metrics: defaultMetrics,
34+
scalingFactor: .mega,
35+
maxDuration: .seconds(10_000_000),
36+
maxIterations: 5
37+
)
38+
) { benchmark in
39+
func noOp() {}
40+
for _ in benchmark.scaledIterations {
41+
eventLoop.execute { noOp() }
42+
}
43+
}
44+
45+
Benchmark(
46+
"MTELG.scheduleTask(in:_:)",
47+
configuration: Benchmark.Configuration(
48+
metrics: defaultMetrics,
49+
scalingFactor: .mega,
50+
maxDuration: .seconds(10_000_000),
51+
maxIterations: 5
52+
)
53+
) { benchmark in
54+
for _ in benchmark.scaledIterations {
55+
eventLoop.scheduleTask(in: .hours(1), {})
56+
}
57+
}
58+
59+
Benchmark(
60+
"MTELG.scheduleCallback(in:_:)",
61+
configuration: Benchmark.Configuration(
62+
metrics: defaultMetrics,
63+
scalingFactor: .mega,
64+
maxDuration: .seconds(10_000_000),
65+
maxIterations: 5
66+
)
67+
) { benchmark in
68+
final class Timer: NIOScheduledCallbackHandler {
69+
func handleScheduledCallback(eventLoop: some EventLoop) {}
70+
}
71+
let timer = Timer()
72+
73+
benchmark.startMeasurement()
74+
for _ in benchmark.scaledIterations {
75+
let handle = try! eventLoop.scheduleCallback(in: .hours(1), handler: timer)
76+
}
77+
}
78+
79+
Benchmark(
80+
"Jump to EL and back using execute and unsafecontinuation",
81+
configuration: .init(
82+
metrics: defaultMetrics,
83+
scalingFactor: .kilo
84+
)
85+
) { benchmark in
86+
for _ in benchmark.scaledIterations {
87+
await withUnsafeContinuation { (continuation: UnsafeContinuation<Void, Never>) in
88+
eventLoop.execute {
89+
continuation.resume()
90+
}
91+
}
92+
}
93+
}
94+
95+
final actor Foo {
96+
nonisolated public let unownedExecutor: UnownedSerialExecutor
97+
98+
init(eventLoop: any EventLoop) {
99+
self.unownedExecutor = eventLoop.executor.asUnownedSerialExecutor()
100+
}
101+
102+
func foo() {
103+
blackHole(Void())
104+
}
105+
}
106+
107+
Benchmark(
108+
"Jump to EL and back using actor with EL executor",
109+
configuration: .init(
110+
metrics: defaultMetrics,
111+
scalingFactor: .kilo
112+
)
113+
) { benchmark in
114+
let actor = Foo(eventLoop: eventLoop)
115+
for _ in benchmark.scaledIterations {
116+
await actor.foo()
117+
}
118+
}
119+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Copyright (c) 2025 PassiveLogic, Inc.
4+
// Licensed under Apache License v2.0
5+
//
6+
// See LICENSE.txt for license information
7+
//
8+
// SPDX-License-Identifier: Apache-2.0
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
// NOTE: By and large the benchmarks here were ported from swift-nio
13+
// to allow side-by-side performance comparison
14+
//
15+
// See https://github.com/apple/swift-nio/blob/main/Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift
16+
17+
#if canImport(Darwin)
18+
import Darwin.C
19+
#elseif canImport(Glibc)
20+
import Glibc
21+
#else
22+
#error("Unsupported platform.")
23+
#endif
24+
25+
// This file allows us to hook the global executor which
26+
// we can use to mimic task executors for now.
27+
typealias EnqueueGlobalHook =
28+
@convention(thin) (UnownedJob, @convention(thin) (UnownedJob) -> Void) -> Void
29+
30+
var swiftTaskEnqueueGlobalHook: EnqueueGlobalHook? {
31+
get { _swiftTaskEnqueueGlobalHook.pointee }
32+
set { _swiftTaskEnqueueGlobalHook.pointee = newValue }
33+
}
34+
35+
private let _swiftTaskEnqueueGlobalHook: UnsafeMutablePointer<EnqueueGlobalHook?> =
36+
dlsym(dlopen(nil, RTLD_LAZY), "swift_task_enqueueGlobal_hook").assumingMemoryBound(
37+
to: EnqueueGlobalHook?.self)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Copyright (c) 2025 PassiveLogic, Inc.
4+
// Licensed under Apache License v2.0
5+
//
6+
// See LICENSE.txt for license information
7+
//
8+
// SPDX-License-Identifier: Apache-2.0
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
// NOTE: By and large the benchmarks here were ported from swift-nio
13+
// to allow side-by-side performance comparison
14+
//
15+
// See https://github.com/apple/swift-nio/blob/main/Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift
16+
17+
import Benchmark
18+
import NIOCore
19+
import NIOEmbedded
20+
21+
let benchmarks = {
22+
let defaultMetrics: [BenchmarkMetric] = [
23+
.mallocCountTotal
24+
]
25+
26+
let leakMetrics: [BenchmarkMetric] = [
27+
.mallocCountTotal,
28+
.memoryLeaked,
29+
]
30+
31+
Benchmark(
32+
"NIOAsyncChannel.init",
33+
configuration: .init(
34+
metrics: defaultMetrics,
35+
scalingFactor: .kilo,
36+
maxDuration: .seconds(10_000_000),
37+
maxIterations: 10
38+
)
39+
) { benchmark in
40+
// Elide the cost of the 'EmbeddedChannel'. It's only used for its pipeline.
41+
var channels: [EmbeddedChannel] = []
42+
channels.reserveCapacity(benchmark.scaledIterations.count)
43+
for _ in 0..<benchmark.scaledIterations.count {
44+
channels.append(EmbeddedChannel())
45+
}
46+
47+
benchmark.startMeasurement()
48+
defer {
49+
benchmark.stopMeasurement()
50+
}
51+
for channel in channels {
52+
let asyncChanel = try NIOAsyncChannel<ByteBuffer, ByteBuffer>(
53+
wrappingChannelSynchronously: channel)
54+
blackHole(asyncChanel)
55+
}
56+
}
57+
58+
Benchmark(
59+
"WaitOnPromise",
60+
configuration: .init(
61+
metrics: leakMetrics,
62+
scalingFactor: .kilo,
63+
maxDuration: .seconds(10_000_000),
64+
maxIterations: 10_000 // need 10k to get a signal
65+
)
66+
) { benchmark in
67+
// Elide the cost of the 'EmbeddedEventLoop'.
68+
let el = EmbeddedEventLoop()
69+
70+
benchmark.startMeasurement()
71+
defer {
72+
benchmark.stopMeasurement()
73+
}
74+
75+
for _ in 0..<benchmark.scaledIterations.count {
76+
let p = el.makePromise(of: Int.self)
77+
p.succeed(0)
78+
do { _ = try! p.futureResult.wait() }
79+
}
80+
}
81+
}

Benchmarks/Package.resolved

Lines changed: 96 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Benchmarks/Package.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// swift-tools-version:5.10
2+
3+
//===----------------------------------------------------------------------===//
4+
//
5+
// Copyright (c) 2025 PassiveLogic, Inc.
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import PackageDescription
15+
16+
let package = Package(
17+
name: "benchmarks",
18+
platforms: [
19+
.macOS("14")
20+
],
21+
dependencies: [
22+
.package(path: "../"),
23+
.package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.22.0"),
24+
],
25+
targets: [
26+
.executableTarget(
27+
name: "NIOAsyncRuntimeBenchmarks",
28+
dependencies: [
29+
.product(name: "Benchmark", package: "package-benchmark"),
30+
.product(name: "NIOAsyncRuntime", package: "nio-async-runtime"),
31+
],
32+
path: "Benchmarks/NIOAsyncRuntimeBenchmarks",
33+
plugins: [
34+
.plugin(name: "BenchmarkPlugin", package: "package-benchmark")
35+
]
36+
)
37+
]
38+
)

Sources/NIOAsyncRuntime/AsyncEventLoop.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,6 @@ public final class AsyncEventLoop: EventLoop, @unchecked Sendable {
353353
init(_ value: T) { self.value = value }
354354
}
355355
}
356+
357+
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
358+
extension AsyncEventLoop: NIOSerialEventLoopExecutor {}

0 commit comments

Comments
 (0)