diff --git a/IntegrationTests/tests_04_performance/Thresholds/5.10.json b/IntegrationTests/tests_04_performance/Thresholds/5.10.json index c3fc7078e3..a13f5e39d5 100644 --- a/IntegrationTests/tests_04_performance/Thresholds/5.10.json +++ b/IntegrationTests/tests_04_performance/Thresholds/5.10.json @@ -32,8 +32,9 @@ "encode_1000_ws_frames_new_buffer_with_space": 3050, "encode_1000_ws_frames_new_buffer_with_space_with_mask": 5050, "execute_hop_10000_tasks": 0, + "future_assume_isolated_lots_of_callbacks": 92050, "future_erase_result": 4050, - "future_lots_of_callbacks": 53050, + "future_lots_of_callbacks": 74050, "get_100000_headers_canonical_form": 700050, "get_100000_headers_canonical_form_trimming_whitespace": 700050, "get_100000_headers_canonical_form_trimming_whitespace_from_long_string": 700050, diff --git a/IntegrationTests/tests_04_performance/Thresholds/5.9.json b/IntegrationTests/tests_04_performance/Thresholds/5.9.json index b9e6d8faa7..82cfbff083 100644 --- a/IntegrationTests/tests_04_performance/Thresholds/5.9.json +++ b/IntegrationTests/tests_04_performance/Thresholds/5.9.json @@ -32,8 +32,9 @@ "encode_1000_ws_frames_new_buffer_with_space": 3050, "encode_1000_ws_frames_new_buffer_with_space_with_mask": 5050, "execute_hop_10000_tasks": 0, + "future_assume_isolated_lots_of_callbacks": 91050, "future_erase_result": 4050, - "future_lots_of_callbacks": 53050, + "future_lots_of_callbacks": 74050, "get_100000_headers_canonical_form": 700050, "get_100000_headers_canonical_form_trimming_whitespace": 700050, "get_100000_headers_canonical_form_trimming_whitespace_from_long_string": 700050, @@ -48,4 +49,3 @@ "udp_1000_reqs_1_conn": 6200, "udp_1_reqs_1000_conn": 162050 } - diff --git a/IntegrationTests/tests_04_performance/Thresholds/6.0.json b/IntegrationTests/tests_04_performance/Thresholds/6.0.json index c3fc7078e3..a13f5e39d5 100644 --- a/IntegrationTests/tests_04_performance/Thresholds/6.0.json +++ b/IntegrationTests/tests_04_performance/Thresholds/6.0.json @@ -32,8 +32,9 @@ "encode_1000_ws_frames_new_buffer_with_space": 3050, "encode_1000_ws_frames_new_buffer_with_space_with_mask": 5050, "execute_hop_10000_tasks": 0, + "future_assume_isolated_lots_of_callbacks": 92050, "future_erase_result": 4050, - "future_lots_of_callbacks": 53050, + "future_lots_of_callbacks": 74050, "get_100000_headers_canonical_form": 700050, "get_100000_headers_canonical_form_trimming_whitespace": 700050, "get_100000_headers_canonical_form_trimming_whitespace_from_long_string": 700050, diff --git a/IntegrationTests/tests_04_performance/Thresholds/nightly-6.0.json b/IntegrationTests/tests_04_performance/Thresholds/nightly-6.0.json index c3fc7078e3..a13f5e39d5 100644 --- a/IntegrationTests/tests_04_performance/Thresholds/nightly-6.0.json +++ b/IntegrationTests/tests_04_performance/Thresholds/nightly-6.0.json @@ -32,8 +32,9 @@ "encode_1000_ws_frames_new_buffer_with_space": 3050, "encode_1000_ws_frames_new_buffer_with_space_with_mask": 5050, "execute_hop_10000_tasks": 0, + "future_assume_isolated_lots_of_callbacks": 92050, "future_erase_result": 4050, - "future_lots_of_callbacks": 53050, + "future_lots_of_callbacks": 74050, "get_100000_headers_canonical_form": 700050, "get_100000_headers_canonical_form_trimming_whitespace": 700050, "get_100000_headers_canonical_form_trimming_whitespace_from_long_string": 700050, diff --git a/IntegrationTests/tests_04_performance/Thresholds/nightly-main.json b/IntegrationTests/tests_04_performance/Thresholds/nightly-main.json index f75dbda93d..1009e21d23 100644 --- a/IntegrationTests/tests_04_performance/Thresholds/nightly-main.json +++ b/IntegrationTests/tests_04_performance/Thresholds/nightly-main.json @@ -32,8 +32,9 @@ "encode_1000_ws_frames_new_buffer_with_space": 3050, "encode_1000_ws_frames_new_buffer_with_space_with_mask": 5050, "execute_hop_10000_tasks": 0, + "future_assume_isolated_lots_of_callbacks": 92050, "future_erase_result": 4050, - "future_lots_of_callbacks": 53050, + "future_lots_of_callbacks": 74050, "get_100000_headers_canonical_form": 500050, "get_100000_headers_canonical_form_trimming_whitespace": 500050, "get_100000_headers_canonical_form_trimming_whitespace_from_long_string": 500050, diff --git a/IntegrationTests/tests_04_performance/test_01_resources/test_future_assume_isolated_lots_of_callbacks.swift b/IntegrationTests/tests_04_performance/test_01_resources/test_future_assume_isolated_lots_of_callbacks.swift new file mode 100644 index 0000000000..d9a04d14e8 --- /dev/null +++ b/IntegrationTests/tests_04_performance/test_01_resources/test_future_assume_isolated_lots_of_callbacks.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2025 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 NIOCore +import NIOEmbedded + +// This test is an equivalent of test_future_lots_of_callbacks.swift. It should +// have the same allocations as that test, and any difference is a bug. +func run(identifier: String) { + measure(identifier: identifier) { + struct MyError: Error {} + @inline(never) + func doThenAndFriends(loop: EventLoop) { + let p = loop.makePromise(of: Int.self) + let f = p.futureResult.assumeIsolated().flatMap { (r: Int) -> EventLoopFuture in + // This call allocates a new Future, and + // so does flatMap(), so this is two Futures. + loop.makeSucceededFuture(r + 1) + }.flatMapThrowing { (r: Int) -> Int in + // flatMapThrowing allocates a new Future, and calls `flatMap` + // which also allocates, so this is two. + r + 2 + }.map { (r: Int) -> Int in + // map allocates a new future, and calls `flatMap` which + // also allocates, so this is two. + r + 2 + }.flatMapThrowing { (r: Int) -> Int in + // flatMapThrowing allocates a future on the error path and + // calls `flatMap`, which also allocates, so this is two. + throw MyError() + }.flatMapError { (err: Error) -> EventLoopFuture in + // This call allocates a new Future, and so does flatMapError, + // so this is two Futures. + loop.makeFailedFuture(err) + }.flatMapErrorThrowing { (err: Error) -> Int? in + // flatMapError allocates a new Future, and calls flatMapError, + // so this is two Futures + throw err + }.recover { (err: Error) -> Int? in + // recover allocates a future, and calls flatMapError, so + // this is two Futures. + nil + }.unwrap { () -> Int in + // unwrap calls map, with an extra closure, so this is three. + 1 + }.always { (Int) -> Void in + // This is a do-nothing call, but it can't be optimised out. + // always calls whenComplete but adds a new closure, so it allocates + // two times. + _ = 1 + 1 + }.flatMapResult { (Int) -> Result in + // flatMapResult allocates a new future and creates a _whenComplete closure, + // so this is two. + .success(5) + } + .unwrap(orReplace: 5) // Same as unwrap above, this is three. + + // Add some when*. + f.whenSuccess { + // whenSuccess should be just one. + _ = $0 + 1 + } + f.whenFailure { _ in + // whenFailure should also be just one. + fatalError() + } + f.whenComplete { + // whenComplete should also be just one. + switch $0 { + case .success: + () + case .failure: + fatalError() + } + } + + p.assumeIsolated().succeed(0) + + // Wait also allocates a lock. + _ = try! f.nonisolated().wait() + } + @inline(never) + func doAnd(loop: EventLoop) { + // This isn't relevant to this test, but we keep it here to keep the numbers lining up. + let p1 = loop.makePromise(of: Int.self) + let p2 = loop.makePromise(of: Int.self) + let p3 = loop.makePromise(of: Int.self) + + // Each call to and() allocates a Future. The calls to + // and(result:) allocate two. + + let f = p1.futureResult + .and(p2.futureResult) + .and(p3.futureResult) + .and(value: 1) + .and(value: 1) + + p1.succeed(1) + p2.succeed(1) + p3.succeed(1) + _ = try! f.wait() + } + + let el = EmbeddedEventLoop() + for _ in 0..<1000 { + doThenAndFriends(loop: el) + doAnd(loop: el) + } + return 1000 + } +} diff --git a/IntegrationTests/tests_04_performance/test_01_resources/test_future_lots_of_callbacks.swift b/IntegrationTests/tests_04_performance/test_01_resources/test_future_lots_of_callbacks.swift index 2e0173ba05..42e8e5731e 100644 --- a/IntegrationTests/tests_04_performance/test_01_resources/test_future_lots_of_callbacks.swift +++ b/IntegrationTests/tests_04_performance/test_01_resources/test_future_lots_of_callbacks.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2025 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -37,19 +37,52 @@ func run(identifier: String) { // flatMapThrowing allocates a future on the error path and // calls `flatMap`, which also allocates, so this is two. throw MyError() - }.flatMapError { (err: Error) -> EventLoopFuture in + }.flatMapError { (err: Error) -> EventLoopFuture in // This call allocates a new Future, and so does flatMapError, // so this is two Futures. loop.makeFailedFuture(err) - }.flatMapErrorThrowing { (err: Error) -> Int in + }.flatMapErrorThrowing { (err: Error) -> Int? in // flatMapError allocates a new Future, and calls flatMapError, // so this is two Futures throw err - }.recover { (err: Error) -> Int in + }.recover { (err: Error) -> Int? in // recover allocates a future, and calls flatMapError, so // this is two Futures. + nil + }.unwrap { () -> Int in + // unwrap calls map, with an extra closure, so this is three. 1 + }.always { (Int) -> Void in + // This is a do-nothing call, but it can't be optimised out. + // always calls whenComplete but adds a new closure, so it allocates + // two times. + _ = 1 + 1 + }.flatMapResult { (Int) -> Result in + // flatMapResult allocates a new future and creates a _whenComplete closure, + // so this is two. + .success(5) } + .unwrap(orReplace: 5) // Same as unwrap above, this is three. + + // Add some when*. + f.whenSuccess { + // whenSuccess should be just one. + _ = $0 + 1 + } + f.whenFailure { _ in + // whenFailure should also be just one. + fatalError() + } + f.whenComplete { + // whenComplete should also be just one. + switch $0 { + case .success: + () + case .failure: + fatalError() + } + } + p.succeed(0) // Wait also allocates a lock.