Specialise succeeded Void futures so we allocate less (#1703)
Motivation: Succeeded `EventLoopFuture<Void>`s are quite important in SwiftNIO, they happen all over the place. Unfortunately, we usually allocate each time, unnecessarily. Modifications: Offer `EventLoop`s the option to cache succeeded void futures. Result: Fewer allocations.
This commit is contained in:
parent
6b4a8197f9
commit
2b821b2ef6
|
@ -266,6 +266,20 @@ public protocol EventLoop: EventLoopGroup {
|
|||
/// Asserts that the current thread is _not_ the one tied to this `EventLoop`.
|
||||
/// Otherwise, the process will be abnormally terminated as per the semantics of `preconditionFailure(_:file:line:)`.
|
||||
func preconditionNotInEventLoop(file: StaticString, line: UInt)
|
||||
|
||||
/// Return a succeeded `Void` future.
|
||||
///
|
||||
/// Semantically, this function is equivalent to calling `makeSucceededFuture(())`.
|
||||
/// Contrary to `makeSucceededFuture`, `makeSucceededVoidFuture` is a customization point for `EventLoop`s which
|
||||
/// allows `EventLoop`s to cache a pre-succeded `Void` future to prevent superfluous allocations.
|
||||
func makeSucceededVoidFuture() -> EventLoopFuture<Void>
|
||||
}
|
||||
|
||||
extension EventLoop {
|
||||
/// Default implementation of `makeSucceededVoidFuture`: Return a fresh future (which will allocate).
|
||||
public func makeSucceededVoidFuture() -> EventLoopFuture<Void> {
|
||||
return EventLoopFuture(eventLoop: self, value: (), file: "n/a", line: 0)
|
||||
}
|
||||
}
|
||||
|
||||
extension EventLoopGroup {
|
||||
|
@ -586,7 +600,12 @@ extension EventLoop {
|
|||
/// - returns: a succeeded `EventLoopFuture`.
|
||||
@inlinable
|
||||
public func makeSucceededFuture<Success>(_ value: Success, file: StaticString = #file, line: UInt = #line) -> EventLoopFuture<Success> {
|
||||
return EventLoopFuture<Success>(eventLoop: self, value: value, file: file, line: line)
|
||||
if Success.self == Void.self {
|
||||
// The as! will always succeed because we previously checked that Success.self == Void.self.
|
||||
return self.makeSucceededVoidFuture() as! EventLoopFuture<Success>
|
||||
} else {
|
||||
return EventLoopFuture<Success>(eventLoop: self, value: value, file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
/// An `EventLoop` forms a singular `EventLoopGroup`, returning itself as the 'next' `EventLoop`.
|
||||
|
|
|
@ -59,6 +59,12 @@ internal final class SelectableEventLoop: EventLoop {
|
|||
@usableFromInline
|
||||
internal var _scheduledTasks = PriorityQueue<ScheduledTask>()
|
||||
private var tasksCopy = ContiguousArray<() -> Void>()
|
||||
@usableFromInline
|
||||
internal var _succeededVoidFuture: Optional<EventLoopFuture<Void>> = nil {
|
||||
didSet {
|
||||
self.assertInEventLoop()
|
||||
}
|
||||
}
|
||||
|
||||
private let canBeShutdownIndividually: Bool
|
||||
@usableFromInline
|
||||
|
@ -150,6 +156,8 @@ internal final class SelectableEventLoop: EventLoop {
|
|||
// We will process 4096 tasks per while loop.
|
||||
self.tasksCopy.reserveCapacity(4096)
|
||||
self.canBeShutdownIndividually = canBeShutdownIndividually
|
||||
// note: We are creating a reference cycle here that we'll break when shutting the SelectableEventLoop down.
|
||||
self._succeededVoidFuture = EventLoopFuture(eventLoop: self, value: (), file: "n/a", line: 0)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
@ -461,6 +469,9 @@ internal final class SelectableEventLoop: EventLoop {
|
|||
|
||||
// This EventLoop was closed so also close the underlying selector.
|
||||
try self._selector.close()
|
||||
|
||||
// This breaks the retain cycle created in `init`.
|
||||
self._succeededVoidFuture = nil
|
||||
}
|
||||
|
||||
internal func initiateClose(queue: DispatchQueue, completionHandler: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
@ -560,6 +571,14 @@ internal final class SelectableEventLoop: EventLoop {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func makeSucceededVoidFuture() -> EventLoopFuture<Void> {
|
||||
guard self.inEventLoop, let voidFuture = self._succeededVoidFuture else {
|
||||
return EventLoopFuture(eventLoop: self, value: (), file: "n/a", line: 0)
|
||||
}
|
||||
return voidFuture
|
||||
}
|
||||
}
|
||||
|
||||
extension SelectableEventLoop: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
|
|
|
@ -79,6 +79,9 @@ extension EventLoopTest {
|
|||
("testSchedulingTaskOnFutureFailedByELShutdownDoesNotMakeUsExplode", testSchedulingTaskOnFutureFailedByELShutdownDoesNotMakeUsExplode),
|
||||
("testEventLoopGroupProvider", testEventLoopGroupProvider),
|
||||
("testScheduleMaximum", testScheduleMaximum),
|
||||
("testEventLoopsWithPreSucceededFuturesCacheThem", testEventLoopsWithPreSucceededFuturesCacheThem),
|
||||
("testEventLoopsWithoutPreSucceededFuturesDoNotCacheThem", testEventLoopsWithoutPreSucceededFuturesDoNotCacheThem),
|
||||
("testSelectableEventLoopHasPreSucceededFuturesOnlyOnTheEventLoop", testSelectableEventLoopHasPreSucceededFuturesOnlyOnTheEventLoop),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1317,4 +1317,142 @@ public final class EventLoopTest : XCTestCase {
|
|||
XCTAssertEqual(error as? EventLoopError, .cancelled)
|
||||
}
|
||||
|
||||
func testEventLoopsWithPreSucceededFuturesCacheThem() {
|
||||
let el = EventLoopWithPreSucceededFuture()
|
||||
defer {
|
||||
XCTAssertNoThrow(try el.syncShutdownGracefully())
|
||||
}
|
||||
|
||||
let future1 = el.makeSucceededFuture(())
|
||||
let future2 = el.makeSucceededFuture(())
|
||||
let future3 = el.makeSucceededVoidFuture()
|
||||
|
||||
XCTAssert(future1 === future2)
|
||||
XCTAssert(future2 === future3)
|
||||
}
|
||||
|
||||
func testEventLoopsWithoutPreSucceededFuturesDoNotCacheThem() {
|
||||
let el = EventLoopWithoutPreSucceededFuture()
|
||||
defer {
|
||||
XCTAssertNoThrow(try el.syncShutdownGracefully())
|
||||
}
|
||||
|
||||
let future1 = el.makeSucceededFuture(())
|
||||
let future2 = el.makeSucceededFuture(())
|
||||
let future3 = el.makeSucceededVoidFuture()
|
||||
|
||||
XCTAssert(future1 !== future2)
|
||||
XCTAssert(future2 !== future3)
|
||||
XCTAssert(future1 !== future3)
|
||||
}
|
||||
|
||||
func testSelectableEventLoopHasPreSucceededFuturesOnlyOnTheEventLoop() {
|
||||
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
defer {
|
||||
XCTAssertNoThrow(try elg.syncShutdownGracefully())
|
||||
}
|
||||
|
||||
let el = elg.next()
|
||||
|
||||
let futureOutside1 = el.makeSucceededVoidFuture()
|
||||
let futureOutside2 = el.makeSucceededFuture(())
|
||||
XCTAssert(futureOutside1 !== futureOutside2)
|
||||
|
||||
XCTAssertNoThrow(try el.submit {
|
||||
let futureInside1 = el.makeSucceededVoidFuture()
|
||||
let futureInside2 = el.makeSucceededFuture(())
|
||||
|
||||
XCTAssert(futureOutside1 !== futureInside1)
|
||||
XCTAssert(futureInside1 === futureInside2)
|
||||
}.wait())
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class EventLoopWithPreSucceededFuture: EventLoop {
|
||||
var inEventLoop: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func execute(_ task: @escaping () -> Void) {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
func submit<T>(_ task: @escaping () throws -> T) -> EventLoopFuture<T> {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func scheduleTask<T>(in: TimeAmount, _ task: @escaping () throws -> T) -> Scheduled<T> {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
func preconditionInEventLoop(file: StaticString, line: UInt) {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
func preconditionNotInEventLoop(file: StaticString, line: UInt) {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
var _succeededVoidFuture: EventLoopFuture<Void>?
|
||||
func makeSucceededVoidFuture() -> EventLoopFuture<Void> {
|
||||
guard self.inEventLoop, let voidFuture = self._succeededVoidFuture else {
|
||||
return self.makeSucceededFuture(())
|
||||
}
|
||||
return voidFuture
|
||||
}
|
||||
|
||||
init() {
|
||||
self._succeededVoidFuture = EventLoopFuture(eventLoop: self, value: (), file: "n/a", line: 0)
|
||||
}
|
||||
|
||||
func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
|
||||
self._succeededVoidFuture = nil
|
||||
queue.async {
|
||||
callback(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class EventLoopWithoutPreSucceededFuture: EventLoop {
|
||||
var inEventLoop: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func execute(_ task: @escaping () -> Void) {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
func submit<T>(_ task: @escaping () throws -> T) -> EventLoopFuture<T> {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func scheduleTask<T>(in: TimeAmount, _ task: @escaping () throws -> T) -> Scheduled<T> {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
func preconditionInEventLoop(file: StaticString, line: UInt) {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
func preconditionNotInEventLoop(file: StaticString, line: UInt) {
|
||||
preconditionFailure("not implemented")
|
||||
}
|
||||
|
||||
func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
|
||||
queue.async {
|
||||
callback(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,11 +21,11 @@ services:
|
|||
- MAX_ALLOCS_ALLOWED_1000_addHandlers=47050
|
||||
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=30540
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpbootstraps=4100
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=181010
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=180010
|
||||
- MAX_ALLOCS_ALLOWED_1000_udp_reqs=16050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpbootstraps=2000
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpconnections=103050
|
||||
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=478050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpconnections=102050
|
||||
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=476050
|
||||
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
|
||||
- MAX_ALLOCS_ALLOWED_creating_10000_headers=100 # 5.2 improvement 10000
|
||||
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
|
||||
|
@ -37,7 +37,7 @@ services:
|
|||
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
|
||||
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=2010 # 5.2 improvement 4000
|
||||
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4440
|
||||
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=210050
|
||||
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=200050
|
||||
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
|
||||
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
|
||||
- MAX_ALLOCS_ALLOWED_udp_1000_reqs_1_conn=16250
|
||||
|
|
|
@ -21,11 +21,11 @@ services:
|
|||
- MAX_ALLOCS_ALLOWED_1000_addHandlers=47050
|
||||
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=30540
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpbootstraps=4100
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=180010
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=179010
|
||||
- MAX_ALLOCS_ALLOWED_1000_udp_reqs=16050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpbootstraps=2000
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpconnections=102050
|
||||
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=473050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpconnections=101050
|
||||
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=471050
|
||||
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
|
||||
- MAX_ALLOCS_ALLOWED_creating_10000_headers=100
|
||||
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
|
||||
|
@ -37,7 +37,7 @@ services:
|
|||
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
|
||||
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=2010
|
||||
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4440
|
||||
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=200500 #5.3 improvement 210050
|
||||
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=190500
|
||||
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
|
||||
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
|
||||
- MAX_ALLOCS_ALLOWED_udp_1000_reqs_1_conn=16250
|
||||
|
|
|
@ -21,11 +21,11 @@ services:
|
|||
- MAX_ALLOCS_ALLOWED_1000_addHandlers=47050
|
||||
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=31990
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpbootstraps=3100
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=189050
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=188050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udp_reqs=18050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpbootstraps=2000
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpconnections=108050
|
||||
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=950050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpconnections=107050
|
||||
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=948050
|
||||
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
|
||||
- MAX_ALLOCS_ALLOWED_creating_10000_headers=10100
|
||||
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
|
||||
|
@ -37,7 +37,7 @@ services:
|
|||
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
|
||||
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=6010
|
||||
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4500
|
||||
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=230050
|
||||
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=220050
|
||||
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
|
||||
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
|
||||
- MAX_ALLOCS_ALLOWED_udp_1000_reqs_1_conn=18250
|
||||
|
|
|
@ -21,11 +21,11 @@ services:
|
|||
- MAX_ALLOCS_ALLOWED_1000_addHandlers=47050
|
||||
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=30540
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpbootstraps=3100
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=181050
|
||||
- MAX_ALLOCS_ALLOWED_1000_tcpconnections=180050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udp_reqs=16050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpbootstraps=2000
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpconnections=103050
|
||||
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=475050
|
||||
- MAX_ALLOCS_ALLOWED_1000_udpconnections=102050
|
||||
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=473050
|
||||
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=2100
|
||||
- MAX_ALLOCS_ALLOWED_creating_10000_headers=10100
|
||||
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
|
||||
|
@ -37,7 +37,7 @@ services:
|
|||
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
|
||||
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=6010
|
||||
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4440
|
||||
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=210050
|
||||
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=200050
|
||||
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
|
||||
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
|
||||
- MAX_ALLOCS_ALLOWED_udp_1000_reqs_1_conn=16250
|
||||
|
|
Loading…
Reference in New Issue