Skip to content

Commit bf7dbeb

Browse files
committed
perf test
1 parent 54d1006 commit bf7dbeb

File tree

4 files changed

+165
-0
lines changed

4 files changed

+165
-0
lines changed

Package.swift

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import PackageDescription
1717

1818
let package = Package(
1919
name: "async-http-client",
20+
platforms: [.macOS(.v13)],
2021
products: [
2122
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]),
2223
],
@@ -32,6 +33,10 @@ let package = Package(
3233
.package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0"),
3334
],
3435
targets: [
36+
.executableTarget(
37+
name: "AsyncHTTPClientPerfTester",
38+
dependencies: ["AsyncHTTPClient"]
39+
),
3540
.target(
3641
name: "CAsyncHTTPClient",
3742
cSettings: [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/// Runs `body` and when body returns or throws, run `finally`.
2+
///
3+
/// This is useful as an async version of this code pattern
4+
///
5+
/// ```swift
6+
/// let resource = try createResource()
7+
/// defer {
8+
/// try? destroyResource()
9+
/// }
10+
/// return try useResource()
11+
/// ```
12+
///
13+
/// - note: Even if the task that `asyncDo` is running is is cancelled, the `finally` code will run in a task that
14+
/// is _not_ cancelled. This is crucial to not require all cleanup code to work in an already-cancelled task.
15+
@inlinable
16+
public func asyncDo<R: Sendable>(
17+
returning: R.Type = R.self,
18+
_ body: @Sendable () async throws -> R,
19+
finally: @escaping @Sendable () async throws -> Void
20+
) async throws -> R {
21+
try await asyncDo {
22+
try await body()
23+
} finally: { _ in
24+
try await finally()
25+
}
26+
}
27+
28+
/// Runs `body` and when body returns or throws, run `finally`.
29+
///
30+
/// This is useful as an async version of this code pattern
31+
///
32+
/// ```swift
33+
/// let resource = try createResource()
34+
/// defer {
35+
/// try? destroyResource()
36+
/// }
37+
/// return try useResource()
38+
/// ```
39+
///
40+
/// - note: Even if the task that `asyncDo` is running is is cancelled, the `finally` code will run in a task that
41+
/// is _not_ cancelled. This is crucial to not require all cleanup code to work in an already-cancelled task.
42+
@inlinable
43+
public func asyncDo<R: Sendable>(
44+
returning: R.Type = R.self,
45+
_ body: @Sendable () async throws -> R,
46+
finally: @escaping @Sendable (Error?) async throws -> Void
47+
) async throws -> R {
48+
let result: R
49+
do {
50+
result = try await body()
51+
} catch {
52+
try? await withUncancelledTask {
53+
try await finally(error)
54+
}
55+
throw error
56+
}
57+
58+
try await withUncancelledTask {
59+
try await finally(nil)
60+
}
61+
return result
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import AsyncHTTPClient
2+
import NIO
3+
import Foundation
4+
5+
func goFuture() throws {
6+
let httpClient = HTTPClient(eventLoopGroup: MultiThreadedEventLoopGroup.singleton)
7+
8+
func doOne() -> EventLoopFuture<Void> {
9+
let request = try! HTTPClient.Request(url: "http://127.0.0.1:8888/")
10+
return httpClient.execute(request: request).map { response in
11+
precondition(response.status.code == 200)
12+
}
13+
}
14+
15+
func doMany(_ count: Int, promise: EventLoopPromise<Void>) -> EventLoopFuture<Void> {
16+
guard count > 0 else {
17+
promise.succeed(())
18+
return promise.futureResult
19+
}
20+
21+
doOne().map {
22+
doMany(count - 1, promise: promise)
23+
}.cascadeFailure(to: promise)
24+
25+
return promise.futureResult
26+
}
27+
28+
let loop = MultiThreadedEventLoopGroup.singleton.any()
29+
try doMany(100_000, promise: loop.makePromise(of: Void.self))
30+
.recover { _ in }
31+
.flatMap {
32+
httpClient.shutdown()
33+
}
34+
.wait()
35+
}
36+
37+
func goAsync() async throws {
38+
let httpClient = HTTPClient(eventLoopGroup: MultiThreadedEventLoopGroup.singleton)
39+
try await asyncDo {
40+
for _ in 0..<100_000 {
41+
let request = HTTPClientRequest(url: "http://127.0.0.1:8888/")
42+
let response = try await httpClient.execute(
43+
request,
44+
deadline: .now() + .seconds(100)
45+
)
46+
for try await _ in response.body {}
47+
precondition(response.status.code == 200)
48+
}
49+
} finally: {
50+
try await httpClient.shutdown()
51+
}
52+
}
53+
54+
@main
55+
struct Main {
56+
static func main() async throws {
57+
var tStart = SuspendingClock.now
58+
try goFuture()
59+
var tEnd = SuspendingClock.now
60+
let diffFuture = tEnd - tStart
61+
62+
precondition(Thread.isMainThread)
63+
let success = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
64+
precondition(success)
65+
66+
tStart = SuspendingClock.now
67+
try await goAsync()
68+
tEnd = SuspendingClock.now
69+
let diffAsync = tEnd - tStart
70+
print("future", diffFuture)
71+
print("async", diffAsync)
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
public func withUncancelledTask<R: Sendable>(
2+
returning: R.Type = R.self,
3+
_ body: @Sendable @escaping () async throws -> R
4+
) async throws -> R {
5+
// This looks unstructured but it isn't, please note that we `await` `.value` of this task.
6+
// The reason we need this separate `Task` is that in general, we cannot assume that code performs to our
7+
// expectations if the task we run it on is already cancelled. However, in some cases we need the code to
8+
// run regardless -- even if our task is already cancelled. Therefore, we create a new, uncancelled task here.
9+
try await Task {
10+
try await body()
11+
}.value
12+
}
13+
14+
public func withUncancelledTask<R: Sendable>(
15+
returning: R.Type = R.self,
16+
_ body: @Sendable @escaping () async -> R
17+
) async -> R {
18+
// This looks unstructured but it isn't, please note that we `await` `.value` of this task.
19+
// The reason we need this separate `Task` is that in general, we cannot assume that code performs to our
20+
// expectations if the task we run it on is already cancelled. However, in some cases we need the code to
21+
// run regardless -- even if our task is already cancelled. Therefore, we create a new, uncancelled task here.
22+
await Task {
23+
await body()
24+
}.value
25+
}

0 commit comments

Comments
 (0)