diff --git a/Package.swift b/Package.swift index 78a72af..1c1a34c 100644 --- a/Package.swift +++ b/Package.swift @@ -34,6 +34,7 @@ let package = Package( .target(name: "NIOSSH", dependencies: ["NIO", "NIOFoundationCompat", "Crypto"]), .target(name: "NIOSSHClient", dependencies: ["NIO", "NIOSSH", "NIOConcurrencyHelpers"]), .target(name: "NIOSSHServer", dependencies: ["NIO", "NIOSSH", "NIOFoundationCompat", "Crypto"]), + .target(name: "NIOSSHPerformanceTester", dependencies: ["NIO", "NIOSSH", "Crypto"]), .testTarget(name: "NIOSSHTests", dependencies: ["NIOSSH", "NIO", "NIOFoundationCompat"]), ] ) diff --git a/Sources/NIOSSHPerformanceTester/Benchmark.swift b/Sources/NIOSSHPerformanceTester/Benchmark.swift new file mode 100644 index 0000000..20e7e78 --- /dev/null +++ b/Sources/NIOSSHPerformanceTester/Benchmark.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2020 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 +// +//===----------------------------------------------------------------------===// + +protocol Benchmark: AnyObject { + func setUp() throws + func tearDown() + func run() throws -> Int +} + +func measureAndPrint(desc: String, benchmark bench: B) throws { + try bench.setUp() + defer { + bench.tearDown() + } + try measureAndPrint(desc: desc) { + try bench.run() + } +} diff --git a/Sources/NIOSSHPerformanceTester/BenchmarkHandshake.swift b/Sources/NIOSSHPerformanceTester/BenchmarkHandshake.swift new file mode 100644 index 0000000..9322d35 --- /dev/null +++ b/Sources/NIOSSHPerformanceTester/BenchmarkHandshake.swift @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2020 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 Crypto +import NIO +import NIOSSH + +final class BenchmarkHandshake: Benchmark { + let serverRole = SSHConnectionRole.server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: ExpectPasswordDelegate("password"))) + let clientRole = SSHConnectionRole.client(.init(userAuthDelegate: RepeatingPasswordDelegate("password"))) + let loopCount: Int + + init(loopCount: Int) { + self.loopCount = loopCount + } + + func setUp() throws {} + + func tearDown() {} + + func run() throws -> Int { + for _ in 0 ..< self.loopCount { + let b2b = BackToBackEmbeddedChannel() + b2b.client.connect(to: try .init(unixDomainSocketPath: "/foo"), promise: nil) + b2b.server.connect(to: try .init(unixDomainSocketPath: "/foo"), promise: nil) + + try b2b.client.pipeline.addHandler(NIOSSHHandler(role: self.clientRole, allocator: b2b.client.allocator, inboundChildChannelInitializer: nil)).wait() + try b2b.server.pipeline.addHandler(NIOSSHHandler(role: self.serverRole, allocator: b2b.server.allocator, inboundChildChannelInitializer: nil)).wait() + try b2b.interactInMemory() + } + + return self.loopCount + } +} diff --git a/Sources/NIOSSHPerformanceTester/main.swift b/Sources/NIOSSHPerformanceTester/main.swift new file mode 100644 index 0000000..2018964 --- /dev/null +++ b/Sources/NIOSSHPerformanceTester/main.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2020 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 Crypto +import Dispatch +import Foundation +import NIO +import NIOSSH + +// MARK: Test Harness + +var warning: String = "" +assert({ + print("======================================================") + print("= YOU ARE RUNNING NIOPerformanceTester IN DEBUG MODE =") + print("======================================================") + warning = " <<< DEBUG MODE >>>" + return true +}()) + +public func measure(_ fn: () throws -> Int) rethrows -> [TimeInterval] { + func measureOne(_ fn: () throws -> Int) rethrows -> TimeInterval { + let start = Date() + _ = try fn() + let end = Date() + return end.timeIntervalSince(start) + } + + _ = try measureOne(fn) /* pre-heat and throw away */ + var measurements = Array(repeating: 0.0, count: 10) + for i in 0 ..< 10 { + measurements[i] = try measureOne(fn) + } + + return measurements +} + +let limitSet = CommandLine.arguments.dropFirst() + +public func measureAndPrint(desc: String, fn: () throws -> Int) rethrows { + if limitSet.count == 0 || limitSet.contains(desc) { + print("measuring\(warning): \(desc): ", terminator: "") + let measurements = try measure(fn) + print(measurements.reduce("") { $0 + "\($1), " }) + } else { + print("skipping '\(desc)', limit set = \(limitSet)") + } +} + +// MARK: Utilities + +try measureAndPrint(desc: "10000_handshakes", benchmark: BenchmarkHandshake(loopCount: 10000)) diff --git a/Sources/NIOSSHPerformanceTester/shared.swift b/Sources/NIOSSHPerformanceTester/shared.swift new file mode 100644 index 0000000..4771520 --- /dev/null +++ b/Sources/NIOSSHPerformanceTester/shared.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2020 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 Crypto +import NIO +import NIOSSH + +class BackToBackEmbeddedChannel { + private(set) var client: EmbeddedChannel + private(set) var server: EmbeddedChannel + private var loop: EmbeddedEventLoop + + init() { + self.loop = EmbeddedEventLoop() + self.client = EmbeddedChannel(loop: self.loop) + self.server = EmbeddedChannel(loop: self.loop) + } + + func run() { + self.loop.run() + } + + func interactInMemory() throws { + var workToDo = true + + while workToDo { + workToDo = false + + self.loop.run() + let clientDatum = try self.client.readOutbound(as: IOData.self) + let serverDatum = try self.server.readOutbound(as: IOData.self) + + if let clientMsg = clientDatum { + try self.server.writeInbound(clientMsg) + workToDo = true + } + + if let serverMsg = serverDatum { + try self.client.writeInbound(serverMsg) + workToDo = true + } + } + } +} + +final class ExpectPasswordDelegate: NIOSSHServerUserAuthenticationDelegate { + let supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods = .password + + let expectedPassword: String + + init(_ expectedPassword: String) { + self.expectedPassword = expectedPassword + } + + func requestReceived(request: NIOSSHUserAuthenticationRequest, responsePromise: EventLoopPromise) { + guard case .password(let password) = request.request, password.password == self.expectedPassword else { + responsePromise.succeed(.failure) + return + } + responsePromise.succeed(.success) + } +} + +final class RepeatingPasswordDelegate: NIOSSHClientUserAuthenticationDelegate { + let password: String + + init(_ password: String) { + self.password = password + } + + func nextAuthenticationType(availableMethods: NIOSSHAvailableUserAuthenticationMethods, nextChallengePromise: EventLoopPromise) { + if availableMethods.contains(.password) { + nextChallengePromise.succeed(.init(username: "foo", serviceName: "ssh-connection", offer: .password(.init(password: self.password)))) + } else { + nextChallengePromise.succeed(nil) + } + } +}