main.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. /*
  2. * Copyright 2019, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import Foundation
  17. import GRPC
  18. import NIO
  19. import NIOSSL
  20. import Commander
  21. struct ConnectionFactory {
  22. var configuration: ClientConnection.Configuration
  23. func makeConnection() -> ClientConnection {
  24. return ClientConnection(configuration: self.configuration)
  25. }
  26. func makeEchoClient() -> Echo_EchoServiceClient {
  27. return Echo_EchoServiceClient(connection: self.makeConnection())
  28. }
  29. }
  30. protocol Benchmark: class {
  31. func setUp() throws
  32. func tearDown() throws
  33. func run() throws
  34. }
  35. /// Tests unary throughput by sending requests on a single connection.
  36. ///
  37. /// Requests are sent in batches of (up-to) 100 requests. This is due to
  38. /// https://github.com/apple/swift-nio-http2/issues/87#issuecomment-483542401.
  39. class UnaryThroughput: Benchmark {
  40. let factory: ConnectionFactory
  41. let requests: Int
  42. let requestLength: Int
  43. var client: Echo_EchoServiceClient!
  44. var request: String!
  45. init(factory: ConnectionFactory, requests: Int, requestLength: Int) {
  46. self.factory = factory
  47. self.requests = requests
  48. self.requestLength = requestLength
  49. }
  50. func setUp() throws {
  51. self.client = self.factory.makeEchoClient()
  52. self.request = String(repeating: "0", count: self.requestLength)
  53. }
  54. func run() throws {
  55. let batchSize = 100
  56. for lowerBound in stride(from: 0, to: self.requests, by: batchSize) {
  57. let upperBound = min(lowerBound + batchSize, self.requests)
  58. let requests = (lowerBound..<upperBound).map { _ in
  59. client.get(Echo_EchoRequest.with { $0.text = self.request }).response
  60. }
  61. try EventLoopFuture.andAllSucceed(requests, on: self.client.connection.eventLoop).wait()
  62. }
  63. }
  64. func tearDown() throws {
  65. try self.client.connection.close().wait()
  66. }
  67. }
  68. /// Tests bidirectional throughput by sending requests over a single stream.
  69. ///
  70. /// Requests are sent in batches of (up-to) 100 requests. This is due to
  71. /// https://github.com/apple/swift-nio-http2/issues/87#issuecomment-483542401.
  72. class BidirectionalThroughput: UnaryThroughput {
  73. override func run() throws {
  74. let update = self.client.update { _ in }
  75. for _ in 0..<self.requests {
  76. update.sendMessage(Echo_EchoRequest.with { $0.text = self.request }, promise: nil)
  77. }
  78. update.sendEnd(promise: nil)
  79. _ = try update.status.wait()
  80. }
  81. }
  82. /// Tests the number of connections that can be created.
  83. final class ConnectionCreationThroughput: Benchmark {
  84. let factory: ConnectionFactory
  85. let connections: Int
  86. var createdConnections: [ClientConnection] = []
  87. class ConnectionReadinessDelegate: ConnectivityStateDelegate {
  88. let promise: EventLoopPromise<Void>
  89. var ready: EventLoopFuture<Void> {
  90. return promise.futureResult
  91. }
  92. init(promise: EventLoopPromise<Void>) {
  93. self.promise = promise
  94. }
  95. func connectivityStateDidChange(from oldState: ConnectivityState, to newState: ConnectivityState) {
  96. switch newState {
  97. case .ready:
  98. promise.succeed(())
  99. case .shutdown:
  100. promise.fail(GRPCStatus(code: .unavailable, message: nil))
  101. default:
  102. break
  103. }
  104. }
  105. }
  106. init(factory: ConnectionFactory, connections: Int) {
  107. self.factory = factory
  108. self.connections = connections
  109. }
  110. func setUp() throws { }
  111. func run() throws {
  112. let connectionsAndDelegates: [(ClientConnection, ConnectionReadinessDelegate)] = (0..<connections).map { _ in
  113. let promise = self.factory.configuration.eventLoopGroup.next().makePromise(of: Void.self)
  114. var configuration = self.factory.configuration
  115. let delegate = ConnectionReadinessDelegate(promise: promise)
  116. configuration.connectivityStateDelegate = delegate
  117. return (ClientConnection(configuration: configuration), delegate)
  118. }
  119. self.createdConnections = connectionsAndDelegates.map { connection, _ in connection }
  120. let futures = connectionsAndDelegates.map { _, delegate in delegate.ready }
  121. try EventLoopFuture.andAllSucceed(
  122. futures,
  123. on: self.factory.configuration.eventLoopGroup.next()
  124. ).wait()
  125. }
  126. func tearDown() throws {
  127. let connectionClosures = self.createdConnections.map {
  128. $0.close()
  129. }
  130. try EventLoopFuture.andAllSucceed(
  131. connectionClosures,
  132. on: self.factory.configuration.eventLoopGroup.next()).wait()
  133. }
  134. }
  135. /// The results of a benchmark.
  136. struct BenchmarkResults {
  137. let benchmarkDescription: String
  138. let durations: [TimeInterval]
  139. /// Returns the results as a comma separated string.
  140. ///
  141. /// The format of the string is as such:
  142. /// <name>, <number of results> [, <duration>]
  143. var asCSV: String {
  144. let items = [self.benchmarkDescription, String(self.durations.count)] + self.durations.map { String($0) }
  145. return items.joined(separator: ", ")
  146. }
  147. }
  148. /// Runs the given benchmark multiple times, recording the wall time for each iteration.
  149. ///
  150. /// - Parameter description: A description of the benchmark.
  151. /// - Parameter benchmark: The benchmark to run.
  152. /// - Parameter repeats: The number of times to run the benchmark.
  153. func measure(description: String, benchmark: Benchmark, repeats: Int) -> BenchmarkResults {
  154. var durations: [TimeInterval] = []
  155. for _ in 0..<repeats {
  156. do {
  157. try benchmark.setUp()
  158. let start = Date()
  159. try benchmark.run()
  160. let end = Date()
  161. durations.append(end.timeIntervalSince(start))
  162. } catch {
  163. // If tearDown fails now then there's not a lot we can do!
  164. try? benchmark.tearDown()
  165. return BenchmarkResults(benchmarkDescription: description, durations: [])
  166. }
  167. do {
  168. try benchmark.tearDown()
  169. } catch {
  170. return BenchmarkResults(benchmarkDescription: description, durations: [])
  171. }
  172. }
  173. return BenchmarkResults(benchmarkDescription: description, durations: durations)
  174. }
  175. /// Makes an SSL context if one is required. Note that the CLI tool doesn't support optional values,
  176. /// so we use empty strings for the paths if we don't require SSL.
  177. ///
  178. /// This function will terminate the program if it is not possible to create an SSL context.
  179. ///
  180. /// - Parameter caCertificatePath: The path to the CA certificate PEM file.
  181. /// - Parameter certificatePath: The path to the certificate.
  182. /// - Parameter privateKeyPath: The path to the private key.
  183. /// - Parameter server: Whether this is for the server or not.
  184. private func makeServerTLSConfiguration(caCertificatePath: String, certificatePath: String, privateKeyPath: String) throws -> Server.Configuration.TLS? {
  185. // Commander doesn't have Optional options; we use empty strings to indicate no value.
  186. guard certificatePath.isEmpty == privateKeyPath.isEmpty &&
  187. privateKeyPath.isEmpty == caCertificatePath.isEmpty else {
  188. print("Paths for CA certificate, certificate and private key must be provided")
  189. exit(1)
  190. }
  191. // No need to check them all because of the guard statement above.
  192. if caCertificatePath.isEmpty {
  193. return nil
  194. }
  195. return .init(
  196. certificateChain: try NIOSSLCertificate.fromPEMFile(certificatePath).map { .certificate($0) },
  197. privateKey: .file(privateKeyPath),
  198. trustRoots: .file(caCertificatePath)
  199. )
  200. }
  201. private func makeClientTLSConfiguration(
  202. caCertificatePath: String,
  203. certificatePath: String,
  204. privateKeyPath: String
  205. ) throws -> ClientConnection.Configuration.TLS? {
  206. // Commander doesn't have Optional options; we use empty strings to indicate no value.
  207. guard certificatePath.isEmpty == privateKeyPath.isEmpty &&
  208. privateKeyPath.isEmpty == caCertificatePath.isEmpty else {
  209. print("Paths for CA certificate, certificate and private key must be provided")
  210. exit(1)
  211. }
  212. // No need to check them all because of the guard statement above.
  213. if caCertificatePath.isEmpty {
  214. return nil
  215. }
  216. return .init(
  217. certificateChain: try NIOSSLCertificate.fromPEMFile(certificatePath).map { .certificate($0) },
  218. privateKey: .file(privateKeyPath),
  219. trustRoots: .file(caCertificatePath)
  220. )
  221. }
  222. enum Benchmarks: String, CaseIterable {
  223. case unaryThroughputSmallRequests = "unary_throughput_small"
  224. case unaryThroughputLargeRequests = "unary_throughput_large"
  225. case bidirectionalThroughputSmallRequests = "bidi_throughput_small"
  226. case bidirectionalThroughputLargeRequests = "bidi_throughput_large"
  227. case connectionThroughput = "connection_throughput"
  228. static let smallRequest = 8
  229. static let largeRequest = 1 << 16
  230. var description: String {
  231. switch self {
  232. case .unaryThroughputSmallRequests:
  233. return "10k unary requests of size \(Benchmarks.smallRequest)"
  234. case .unaryThroughputLargeRequests:
  235. return "10k unary requests of size \(Benchmarks.largeRequest)"
  236. case .bidirectionalThroughputSmallRequests:
  237. return "20k bidirectional messages of size \(Benchmarks.smallRequest)"
  238. case .bidirectionalThroughputLargeRequests:
  239. return "10k bidirectional messages of size \(Benchmarks.largeRequest)"
  240. case .connectionThroughput:
  241. return "100 connections created"
  242. }
  243. }
  244. func makeBenchmark(factory: ConnectionFactory) -> Benchmark {
  245. switch self {
  246. case .unaryThroughputSmallRequests:
  247. return UnaryThroughput(factory: factory, requests: 10_000, requestLength: Benchmarks.smallRequest)
  248. case .unaryThroughputLargeRequests:
  249. return UnaryThroughput(factory: factory, requests: 10_000, requestLength: Benchmarks.largeRequest)
  250. case .bidirectionalThroughputSmallRequests:
  251. return BidirectionalThroughput(factory: factory, requests: 20_000, requestLength: Benchmarks.smallRequest)
  252. case .bidirectionalThroughputLargeRequests:
  253. return BidirectionalThroughput(factory: factory, requests: 10_000, requestLength: Benchmarks.largeRequest)
  254. case .connectionThroughput:
  255. return ConnectionCreationThroughput(factory: factory, connections: 100)
  256. }
  257. }
  258. func run(using factory: ConnectionFactory, repeats: Int = 10) -> BenchmarkResults {
  259. let benchmark = self.makeBenchmark(factory: factory)
  260. return measure(description: self.description, benchmark: benchmark, repeats: repeats)
  261. }
  262. }
  263. let hostOption = Option(
  264. "host",
  265. // Use IPv4 to avoid the happy eyeballs delay, this is important when we test the
  266. // connection throughput.
  267. default: "127.0.0.1",
  268. description: "The host to connect to.")
  269. let portOption = Option(
  270. "port",
  271. default: 8080,
  272. description: "The port on the host to connect to.")
  273. let benchmarkOption = Option(
  274. "benchmarks",
  275. default: Benchmarks.allCases.map { $0.rawValue }.joined(separator: ","),
  276. description: "A comma separated list of benchmarks to run. Defaults to all benchmarks.")
  277. let caCertificateOption = Option(
  278. "ca_certificate",
  279. default: "",
  280. description: "The path to the CA certificate to use.")
  281. let certificateOption = Option(
  282. "certificate",
  283. default: "",
  284. description: "The path to the certificate to use.")
  285. let privateKeyOption = Option(
  286. "private_key",
  287. default: "",
  288. description: "The path to the private key to use.")
  289. let hostOverrideOption = Option(
  290. "hostname_override",
  291. default: "",
  292. description: "The expected name of the server to use for TLS.")
  293. Group { group in
  294. group.command(
  295. "run_benchmarks",
  296. benchmarkOption,
  297. hostOption,
  298. portOption,
  299. caCertificateOption,
  300. certificateOption,
  301. privateKeyOption,
  302. hostOverrideOption
  303. ) { benchmarkNames, host, port, caCertificatePath, certificatePath, privateKeyPath, hostOverride in
  304. let tlsConfiguration = try makeClientTLSConfiguration(
  305. caCertificatePath: caCertificatePath,
  306. certificatePath: certificatePath,
  307. privateKeyPath: privateKeyPath)
  308. let configuration = ClientConnection.Configuration(
  309. target: .hostAndPort(host, port),
  310. eventLoopGroup: MultiThreadedEventLoopGroup(numberOfThreads: 1),
  311. tls: tlsConfiguration)
  312. let factory = ConnectionFactory(configuration: configuration)
  313. let names = benchmarkNames.components(separatedBy: ",")
  314. // validate the benchmarks exist before running any
  315. let benchmarks = names.map { name -> Benchmarks in
  316. guard let benchnark = Benchmarks(rawValue: name) else {
  317. print("unknown benchmark: \(name)")
  318. exit(1)
  319. }
  320. return benchnark
  321. }
  322. benchmarks.forEach { benchmark in
  323. let results = benchmark.run(using: factory)
  324. print(results.asCSV)
  325. }
  326. }
  327. group.command(
  328. "start_server",
  329. hostOption,
  330. portOption,
  331. caCertificateOption,
  332. certificateOption,
  333. privateKeyOption
  334. ) { host, port, caCertificatePath, certificatePath, privateKeyPath in
  335. let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
  336. let tlsConfiguration = try makeServerTLSConfiguration(
  337. caCertificatePath: caCertificatePath,
  338. certificatePath: certificatePath,
  339. privateKeyPath: privateKeyPath)
  340. let configuration = Server.Configuration(
  341. target: .hostAndPort(host, port),
  342. eventLoopGroup: group,
  343. serviceProviders: [EchoProvider()],
  344. tls: tlsConfiguration)
  345. let server: Server
  346. do {
  347. server = try Server.start(configuration: configuration).wait()
  348. } catch {
  349. print("unable to start server: \(error)")
  350. exit(1)
  351. }
  352. print("server started on port: \(server.channel.localAddress?.port ?? port)")
  353. // Stop the program from exiting.
  354. try? server.onClose.wait()
  355. }
  356. }.run()