Skip to content

Commit

Permalink
Test allocations of isolated ELF views (#3054)
Browse files Browse the repository at this point in the history
Motivation:

We've added isolated views onto EventLoopFutures. These are great, but
as initially implemented they add a bunch of overhead to the nonisolated
versions. That's not optimal, but before we change the code we should
make sure we have a benchmark baseline.

Modifications:

- Extend the test_future_lots_of_callbacks alloc test to add all the
callbacks that the isolated views have.
- Duplicate that test to one that adds isolated wrappers so we can
compare.

Result:

Two regression tests whose allocation counts should be equal, but
aren't.
  • Loading branch information
Lukasa authored Jan 13, 2025
1 parent 329ef54 commit 588523e
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 10 deletions.
3 changes: 2 additions & 1 deletion IntegrationTests/tests_04_performance/Thresholds/5.10.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions IntegrationTests/tests_04_performance/Thresholds/5.9.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -48,4 +49,3 @@
"udp_1000_reqs_1_conn": 6200,
"udp_1_reqs_1000_conn": 162050
}

3 changes: 2 additions & 1 deletion IntegrationTests/tests_04_performance/Thresholds/6.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Int> 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<Int?> 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<Int?, Error> 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Int> in
}.flatMapError { (err: Error) -> EventLoopFuture<Int?> 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<Int?, Error> 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.
Expand Down

0 comments on commit 588523e

Please sign in to comment.