main.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  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 EchoImplementation
  21. import EchoModel
  22. import Logging
  23. struct ConnectionFactory {
  24. var configuration: ClientConnection.Configuration
  25. func makeConnection() -> ClientConnection {
  26. return ClientConnection(configuration: self.configuration)
  27. }
  28. func makeEchoClient() -> Echo_EchoServiceClient {
  29. return Echo_EchoServiceClient(connection: self.makeConnection())
  30. }
  31. }
  32. protocol Benchmark: class {
  33. func setUp() throws
  34. func tearDown() throws
  35. func run() throws
  36. }
  37. /// Tests unary throughput by sending requests on a single connection.
  38. ///
  39. /// Requests are sent in batches of (up-to) 100 requests. This is due to
  40. /// https://github.com/apple/swift-nio-http2/issues/87#issuecomment-483542401.
  41. class UnaryThroughput: Benchmark {
  42. let factory: ConnectionFactory
  43. let requests: Int
  44. let requestLength: Int
  45. var client: Echo_EchoServiceClient!
  46. var request: String!
  47. init(factory: ConnectionFactory, requests: Int, requestLength: Int) {
  48. self.factory = factory
  49. self.requests = requests
  50. self.requestLength = requestLength
  51. }
  52. func setUp() throws {
  53. self.client = self.factory.makeEchoClient()
  54. self.request = String(repeating: "0", count: self.requestLength)
  55. }
  56. func run() throws {
  57. let batchSize = 100
  58. for lowerBound in stride(from: 0, to: self.requests, by: batchSize) {
  59. let upperBound = min(lowerBound + batchSize, self.requests)
  60. let requests = (lowerBound..<upperBound).map { _ in
  61. client.get(Echo_EchoRequest.with { $0.text = self.request }).response
  62. }
  63. try EventLoopFuture.andAllSucceed(requests, on: self.client.connection.eventLoop).wait()
  64. }
  65. }
  66. func tearDown() throws {
  67. try self.client.connection.close().wait()
  68. }
  69. }
  70. /// Tests bidirectional throughput by sending requests over a single stream.
  71. ///
  72. /// Requests are sent in batches of (up-to) 100 requests. This is due to
  73. /// https://github.com/apple/swift-nio-http2/issues/87#issuecomment-483542401.
  74. class BidirectionalThroughput: UnaryThroughput {
  75. override func run() throws {
  76. let update = self.client.update { _ in }
  77. for _ in 0..<self.requests {
  78. update.sendMessage(Echo_EchoRequest.with { $0.text = self.request }, promise: nil)
  79. }
  80. update.sendEnd(promise: nil)
  81. _ = try update.status.wait()
  82. }
  83. }
  84. /// Tests the number of connections that can be created.
  85. final class ConnectionCreationThroughput: Benchmark {
  86. let factory: ConnectionFactory
  87. let connections: Int
  88. var createdConnections: [ClientConnection] = []
  89. class ConnectionReadinessDelegate: ConnectivityStateDelegate {
  90. let promise: EventLoopPromise<Void>
  91. var ready: EventLoopFuture<Void> {
  92. return promise.futureResult
  93. }
  94. init(promise: EventLoopPromise<Void>) {
  95. self.promise = promise
  96. }
  97. func connectivityStateDidChange(from oldState: ConnectivityState, to newState: ConnectivityState) {
  98. switch newState {
  99. case .ready:
  100. promise.succeed(())
  101. case .shutdown:
  102. promise.fail(GRPCStatus(code: .unavailable, message: nil))
  103. default:
  104. break
  105. }
  106. }
  107. }
  108. init(factory: ConnectionFactory, connections: Int) {
  109. self.factory = factory
  110. self.connections = connections
  111. }
  112. func setUp() throws { }
  113. func run() throws {
  114. let connectionsAndDelegates: [(ClientConnection, ConnectionReadinessDelegate)] = (0..<connections).map { _ in
  115. let promise = self.factory.configuration.eventLoopGroup.next().makePromise(of: Void.self)
  116. var configuration = self.factory.configuration
  117. let delegate = ConnectionReadinessDelegate(promise: promise)
  118. configuration.connectivityStateDelegate = delegate
  119. return (ClientConnection(configuration: configuration), delegate)
  120. }
  121. self.createdConnections = connectionsAndDelegates.map { connection, _ in connection }
  122. let futures = connectionsAndDelegates.map { _, delegate in delegate.ready }
  123. try EventLoopFuture.andAllSucceed(
  124. futures,
  125. on: self.factory.configuration.eventLoopGroup.next()
  126. ).wait()
  127. }
  128. func tearDown() throws {
  129. let connectionClosures = self.createdConnections.map {
  130. $0.close()
  131. }
  132. try EventLoopFuture.andAllSucceed(
  133. connectionClosures,
  134. on: self.factory.configuration.eventLoopGroup.next()).wait()
  135. }
  136. }
  137. /// The results of a benchmark.
  138. struct BenchmarkResults {
  139. let benchmarkDescription: String
  140. let durations: [TimeInterval]
  141. /// Returns the results as a comma separated string.
  142. ///
  143. /// The format of the string is as such:
  144. /// <name>, <number of results> [, <duration>]
  145. var asCSV: String {
  146. let items = [self.benchmarkDescription, String(self.durations.count)] + self.durations.map { String($0) }
  147. return items.joined(separator: ", ")
  148. }
  149. }
  150. /// Runs the given benchmark multiple times, recording the wall time for each iteration.
  151. ///
  152. /// - Parameter description: A description of the benchmark.
  153. /// - Parameter benchmark: The benchmark to run.
  154. /// - Parameter repeats: The number of times to run the benchmark.
  155. func measure(description: String, benchmark: Benchmark, repeats: Int) -> BenchmarkResults {
  156. var durations: [TimeInterval] = []
  157. for _ in 0..<repeats {
  158. do {
  159. try benchmark.setUp()
  160. let start = Date()
  161. try benchmark.run()
  162. let end = Date()
  163. durations.append(end.timeIntervalSince(start))
  164. } catch {
  165. // If tearDown fails now then there's not a lot we can do!
  166. try? benchmark.tearDown()
  167. return BenchmarkResults(benchmarkDescription: description, durations: [])
  168. }
  169. do {
  170. try benchmark.tearDown()
  171. } catch {
  172. return BenchmarkResults(benchmarkDescription: description, durations: [])
  173. }
  174. }
  175. return BenchmarkResults(benchmarkDescription: description, durations: durations)
  176. }
  177. /// Makes an SSL context if one is required. Note that the CLI tool doesn't support optional values,
  178. /// so we use empty strings for the paths if we don't require SSL.
  179. ///
  180. /// This function will terminate the program if it is not possible to create an SSL context.
  181. ///
  182. /// - Parameter caCertificatePath: The path to the CA certificate PEM file.
  183. /// - Parameter certificatePath: The path to the certificate.
  184. /// - Parameter privateKeyPath: The path to the private key.
  185. /// - Parameter server: Whether this is for the server or not.
  186. private func makeServerTLSConfiguration(caCertificatePath: String, certificatePath: String, privateKeyPath: String) throws -> Server.Configuration.TLS? {
  187. // Commander doesn't have Optional options; we use empty strings to indicate no value.
  188. guard certificatePath.isEmpty == privateKeyPath.isEmpty &&
  189. privateKeyPath.isEmpty == caCertificatePath.isEmpty else {
  190. print("Paths for CA certificate, certificate and private key must be provided")
  191. exit(1)
  192. }
  193. // No need to check them all because of the guard statement above.
  194. if caCertificatePath.isEmpty {
  195. return nil
  196. }
  197. return .init(
  198. certificateChain: try NIOSSLCertificate.fromPEMFile(certificatePath).map { .certificate($0) },
  199. privateKey: .file(privateKeyPath),
  200. trustRoots: .file(caCertificatePath)
  201. )
  202. }
  203. private func makeClientTLSConfiguration(
  204. caCertificatePath: String,
  205. certificatePath: String,
  206. privateKeyPath: String
  207. ) throws -> ClientConnection.Configuration.TLS? {
  208. // Commander doesn't have Optional options; we use empty strings to indicate no value.
  209. guard certificatePath.isEmpty == privateKeyPath.isEmpty &&
  210. privateKeyPath.isEmpty == caCertificatePath.isEmpty else {
  211. print("Paths for CA certificate, certificate and private key must be provided")
  212. exit(1)
  213. }
  214. // No need to check them all because of the guard statement above.
  215. if caCertificatePath.isEmpty {
  216. return nil
  217. }
  218. return .init(
  219. certificateChain: try NIOSSLCertificate.fromPEMFile(certificatePath).map { .certificate($0) },
  220. privateKey: .file(privateKeyPath),
  221. trustRoots: .file(caCertificatePath)
  222. )
  223. }
  224. enum Benchmarks: String, CaseIterable {
  225. case unaryThroughputSmallRequests = "unary_throughput_small"
  226. case unaryThroughputLargeRequests = "unary_throughput_large"
  227. case bidirectionalThroughputSmallRequests = "bidi_throughput_small"
  228. case bidirectionalThroughputLargeRequests = "bidi_throughput_large"
  229. case connectionThroughput = "connection_throughput"
  230. static let smallRequest = 8
  231. static let largeRequest = 1 << 16
  232. var description: String {
  233. switch self {
  234. case .unaryThroughputSmallRequests:
  235. return "10k unary requests of size \(Benchmarks.smallRequest)"
  236. case .unaryThroughputLargeRequests:
  237. return "10k unary requests of size \(Benchmarks.largeRequest)"
  238. case .bidirectionalThroughputSmallRequests:
  239. return "20k bidirectional messages of size \(Benchmarks.smallRequest)"
  240. case .bidirectionalThroughputLargeRequests:
  241. return "10k bidirectional messages of size \(Benchmarks.largeRequest)"
  242. case .connectionThroughput:
  243. return "100 connections created"
  244. }
  245. }
  246. func makeBenchmark(factory: ConnectionFactory) -> Benchmark {
  247. switch self {
  248. case .unaryThroughputSmallRequests:
  249. return UnaryThroughput(factory: factory, requests: 10_000, requestLength: Benchmarks.smallRequest)
  250. case .unaryThroughputLargeRequests:
  251. return UnaryThroughput(factory: factory, requests: 10_000, requestLength: Benchmarks.largeRequest)
  252. case .bidirectionalThroughputSmallRequests:
  253. return BidirectionalThroughput(factory: factory, requests: 20_000, requestLength: Benchmarks.smallRequest)
  254. case .bidirectionalThroughputLargeRequests:
  255. return BidirectionalThroughput(factory: factory, requests: 10_000, requestLength: Benchmarks.largeRequest)
  256. case .connectionThroughput:
  257. return ConnectionCreationThroughput(factory: factory, connections: 100)
  258. }
  259. }
  260. func run(using factory: ConnectionFactory, repeats: Int = 10) -> BenchmarkResults {
  261. let benchmark = self.makeBenchmark(factory: factory)
  262. return measure(description: self.description, benchmark: benchmark, repeats: repeats)
  263. }
  264. }
  265. enum Command {
  266. case listBenchmarks
  267. case benchmark(name: String, host: String, port: Int, tls: (ca: String, cert: String)?)
  268. case server(port: Int, tls: (ca: String, cert: String, key: String)?)
  269. init?(from args: [String]) {
  270. guard !args.isEmpty else {
  271. return nil
  272. }
  273. var args = args
  274. let command = args.removeFirst()
  275. switch command {
  276. case "server":
  277. guard let port = args.popLast().flatMap(Int.init) else {
  278. return nil
  279. }
  280. let caPath = args.suffixOfFirst(prefixedWith: "--caPath=")
  281. let certPath = args.suffixOfFirst(prefixedWith: "--certPath=")
  282. let keyPath = args.suffixOfFirst(prefixedWith: "--keyPath=")
  283. // We need all or nothing here:
  284. switch (caPath, certPath, keyPath) {
  285. case let (.some(ca), .some(cert), .some(key)):
  286. self = .server(port: port, tls: (ca: ca, cert: cert, key: key))
  287. case (.none, .none, .none):
  288. self = .server(port: port, tls: nil)
  289. default:
  290. return nil
  291. }
  292. case "benchmark":
  293. guard let name = args.popLast(),
  294. let port = args.popLast().flatMap(Int.init),
  295. let host = args.popLast()
  296. else {
  297. return nil
  298. }
  299. let caPath = args.suffixOfFirst(prefixedWith: "--caPath=")
  300. let certPath = args.suffixOfFirst(prefixedWith: "--certPath=")
  301. // We need all or nothing here:
  302. switch (caPath, certPath) {
  303. case let (.some(ca), .some(cert)):
  304. self = .benchmark(name: name, host: host, port: port, tls: (ca: ca, cert: cert))
  305. case (.none, .none):
  306. self = .benchmark(name: name, host: host, port: port, tls: nil)
  307. default:
  308. return nil
  309. }
  310. case "list_benchmarks":
  311. self = .listBenchmarks
  312. default:
  313. return nil
  314. }
  315. }
  316. }
  317. func printUsageAndExit(program: String) -> Never {
  318. print("""
  319. Usage: \(program) COMMAND [OPTIONS...]
  320. benchmark:
  321. Run the given benchmark (see 'list_benchmarks' for possible options) against a server on the
  322. specified host and port. TLS may be used by spefifying the path to the PEM formatted
  323. certificate and CA certificate.
  324. benchmark [--ca=CA --cert=CERT] HOST PORT BENCHMARK_NAME
  325. Note: eiether all or none of CA and CERT must be provided.
  326. list_benchmarks:
  327. List the available benchmarks to run.
  328. server:
  329. Start the server on the given PORT. TLS may be used by specifying the paths to the PEM formatted
  330. certificate, private key and CA certificate.
  331. server [--ca=CA --cert=CERT --key=KEY] PORT
  332. Note: eiether all or none of CA, CERT and KEY must be provided.
  333. """)
  334. exit(1)
  335. }
  336. fileprivate extension Array where Element == String {
  337. func suffixOfFirst(prefixedWith prefix: String) -> String? {
  338. return self.first {
  339. $0.hasPrefix(prefix)
  340. }.map {
  341. String($0.dropFirst(prefix.count))
  342. }
  343. }
  344. }
  345. func main(args: [String]) {
  346. var args = args
  347. let program = args.removeFirst()
  348. guard let command = Command(from: args) else {
  349. printUsageAndExit(program: program)
  350. }
  351. switch command {
  352. case let .server(port: port, tls: tls):
  353. let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
  354. defer {
  355. try! group.syncShutdownGracefully()
  356. }
  357. // Quieten the logs.
  358. LoggingSystem.bootstrap {
  359. var handler = StreamLogHandler.standardOutput(label: $0)
  360. handler.logLevel = .warning
  361. return handler
  362. }
  363. do {
  364. let configuration = try Server.Configuration(
  365. target: .hostAndPort("localhost", port),
  366. eventLoopGroup: group,
  367. serviceProviders: [EchoProvider()],
  368. tls: tls.map { tlsArgs in
  369. return .init(
  370. certificateChain: try NIOSSLCertificate.fromPEMFile(tlsArgs.cert).map { .certificate($0) },
  371. privateKey: .file(tlsArgs.key),
  372. trustRoots: .file(tlsArgs.ca)
  373. )
  374. }
  375. )
  376. let server = try Server.start(configuration: configuration).wait()
  377. print("server started on port: \(server.channel.localAddress?.port ?? port)")
  378. // Stop the program from exiting.
  379. try? server.onClose.wait()
  380. } catch {
  381. print("unable to start server: \(error)")
  382. exit(1)
  383. }
  384. case let .benchmark(name: name, host: host, port: port, tls: tls):
  385. guard let benchmark = Benchmarks(rawValue: name) else {
  386. printUsageAndExit(program: program)
  387. }
  388. // Quieten the logs.
  389. LoggingSystem.bootstrap {
  390. var handler = StreamLogHandler.standardOutput(label: $0)
  391. handler.logLevel = .critical
  392. return handler
  393. }
  394. let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
  395. defer {
  396. try! group.syncShutdownGracefully()
  397. }
  398. do {
  399. let configuration = try ClientConnection.Configuration(
  400. target: .hostAndPort(host, port),
  401. eventLoopGroup: group,
  402. tls: tls.map { tlsArgs in
  403. return .init(
  404. certificateChain: try NIOSSLCertificate.fromPEMFile(tlsArgs.cert).map { .certificate($0) },
  405. trustRoots: .file(tlsArgs.ca)
  406. )
  407. }
  408. )
  409. let factory = ConnectionFactory(configuration: configuration)
  410. let results = benchmark.run(using: factory)
  411. print(results.asCSV)
  412. } catch {
  413. print("unable to run benchmark: \(error)")
  414. exit(1)
  415. }
  416. case .listBenchmarks:
  417. Benchmarks.allCases.forEach {
  418. print($0.rawValue)
  419. }
  420. }
  421. }
  422. main(args: CommandLine.arguments)