diff --git a/Benchmarks/.gitignore b/Benchmarks/.gitignore new file mode 100644 index 0000000..2517bcd --- /dev/null +++ b/Benchmarks/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +.benchmarkBaselines/ \ No newline at end of file diff --git a/Benchmarks/Benchmarks/NIOAsyncRuntimeBenchmarks/Benchmarks.swift b/Benchmarks/Benchmarks/NIOAsyncRuntimeBenchmarks/Benchmarks.swift new file mode 100644 index 0000000..532a7c2 --- /dev/null +++ b/Benchmarks/Benchmarks/NIOAsyncRuntimeBenchmarks/Benchmarks.swift @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// Copyright (c) 2025 PassiveLogic, Inc. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// NOTE: By and large the benchmarks here were ported from swift-nio +// to allow side-by-side performance comparison +// +// See https://github.com/apple/swift-nio/blob/main/Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift + +import Benchmark +import NIOAsyncRuntime +import NIOCore + +private let eventLoop = MultiThreadedEventLoopGroup.singleton.next() + +let benchmarks = { + let defaultMetrics: [BenchmarkMetric] = [ + .mallocCountTotal, + .contextSwitches, + .wallClock, + ] + + Benchmark( + "MTELG.immediateTasksThroughput", + configuration: Benchmark.Configuration( + metrics: defaultMetrics, + scalingFactor: .mega, + maxDuration: .seconds(10_000_000), + maxIterations: 5 + ) + ) { benchmark in + func noOp() {} + for _ in benchmark.scaledIterations { + eventLoop.execute { noOp() } + } + } + + Benchmark( + "MTELG.scheduleTask(in:_:)", + configuration: Benchmark.Configuration( + metrics: defaultMetrics, + scalingFactor: .mega, + maxDuration: .seconds(10_000_000), + maxIterations: 5 + ) + ) { benchmark in + for _ in benchmark.scaledIterations { + eventLoop.scheduleTask(in: .hours(1), {}) + } + } + + Benchmark( + "MTELG.scheduleCallback(in:_:)", + configuration: Benchmark.Configuration( + metrics: defaultMetrics, + scalingFactor: .mega, + maxDuration: .seconds(10_000_000), + maxIterations: 5 + ) + ) { benchmark in + final class Timer: NIOScheduledCallbackHandler { + func handleScheduledCallback(eventLoop: some EventLoop) {} + } + let timer = Timer() + + benchmark.startMeasurement() + for _ in benchmark.scaledIterations { + let handle = try! eventLoop.scheduleCallback(in: .hours(1), handler: timer) + } + } + + Benchmark( + "Jump to EL and back using execute and unsafecontinuation", + configuration: .init( + metrics: defaultMetrics, + scalingFactor: .kilo + ) + ) { benchmark in + for _ in benchmark.scaledIterations { + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + eventLoop.execute { + continuation.resume() + } + } + } + } + + final actor Foo { + nonisolated public let unownedExecutor: UnownedSerialExecutor + + init(eventLoop: any EventLoop) { + self.unownedExecutor = eventLoop.executor.asUnownedSerialExecutor() + } + + func foo() { + blackHole(Void()) + } + } + + Benchmark( + "Jump to EL and back using actor with EL executor", + configuration: .init( + metrics: defaultMetrics, + scalingFactor: .kilo + ) + ) { benchmark in + let actor = Foo(eventLoop: eventLoop) + for _ in benchmark.scaledIterations { + await actor.foo() + } + } +} diff --git a/Benchmarks/Benchmarks/NIOAsyncRuntimeBenchmarks/Util/GlobalExecutor.swift b/Benchmarks/Benchmarks/NIOAsyncRuntimeBenchmarks/Util/GlobalExecutor.swift new file mode 100644 index 0000000..935cf5d --- /dev/null +++ b/Benchmarks/Benchmarks/NIOAsyncRuntimeBenchmarks/Util/GlobalExecutor.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// Copyright (c) 2025 PassiveLogic, Inc. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// NOTE: By and large the benchmarks here were ported from swift-nio +// to allow side-by-side performance comparison +// +// See https://github.com/apple/swift-nio/blob/main/Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift + +#if canImport(Darwin) + import Darwin.C +#elseif canImport(Glibc) + import Glibc +#else + #error("Unsupported platform.") +#endif + +// This file allows us to hook the global executor which +// we can use to mimic task executors for now. +typealias EnqueueGlobalHook = + @convention(thin) (UnownedJob, @convention(thin) (UnownedJob) -> Void) -> Void + +var swiftTaskEnqueueGlobalHook: EnqueueGlobalHook? { + get { _swiftTaskEnqueueGlobalHook.pointee } + set { _swiftTaskEnqueueGlobalHook.pointee = newValue } +} + +private let _swiftTaskEnqueueGlobalHook: UnsafeMutablePointer = + dlsym(dlopen(nil, RTLD_LAZY), "swift_task_enqueueGlobal_hook").assumingMemoryBound( + to: EnqueueGlobalHook?.self) diff --git a/Benchmarks/Benchmarks/NIOCoreBenchmarks/Benchmarks.swift b/Benchmarks/Benchmarks/NIOCoreBenchmarks/Benchmarks.swift new file mode 100644 index 0000000..d587f65 --- /dev/null +++ b/Benchmarks/Benchmarks/NIOCoreBenchmarks/Benchmarks.swift @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// Copyright (c) 2025 PassiveLogic, Inc. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// NOTE: By and large the benchmarks here were ported from swift-nio +// to allow side-by-side performance comparison +// +// See https://github.com/apple/swift-nio/blob/main/Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift + +import Benchmark +import NIOCore +import NIOEmbedded + +let benchmarks = { + let defaultMetrics: [BenchmarkMetric] = [ + .mallocCountTotal + ] + + let leakMetrics: [BenchmarkMetric] = [ + .mallocCountTotal, + .memoryLeaked, + ] + + Benchmark( + "NIOAsyncChannel.init", + configuration: .init( + metrics: defaultMetrics, + scalingFactor: .kilo, + maxDuration: .seconds(10_000_000), + maxIterations: 10 + ) + ) { benchmark in + // Elide the cost of the 'EmbeddedChannel'. It's only used for its pipeline. + var channels: [EmbeddedChannel] = [] + channels.reserveCapacity(benchmark.scaledIterations.count) + for _ in 0..( + wrappingChannelSynchronously: channel) + blackHole(asyncChanel) + } + } + + Benchmark( + "WaitOnPromise", + configuration: .init( + metrics: leakMetrics, + scalingFactor: .kilo, + maxDuration: .seconds(10_000_000), + maxIterations: 10_000 // need 10k to get a signal + ) + ) { benchmark in + // Elide the cost of the 'EmbeddedEventLoop'. + let el = EmbeddedEventLoop() + + benchmark.startMeasurement() + defer { + benchmark.stopMeasurement() + } + + for _ in 0..